Compare commits

581 Commits

Author SHA1 Message Date
72ad73da8c switch to localhost backend 2026-04-18 08:31:56 -06:00
e4cfd0ccb1 Merge pull request #324 from RecentRunner/refactor/backend-cleanup
backend DRY/KISS cleanup
2026-04-18 08:23:37 -06:00
1eb5c4fe12 fix compatibility regressions 2026-04-17 18:52:00 -06:00
f385217234 extract image delete to storage 2026-04-17 18:24:00 -06:00
482159fbe2 add read-only transactional annotations 2026-04-17 17:58:00 -06:00
cd1216e19f simplify controllers and utilities 2026-04-17 17:35:00 -06:00
c03e5b0f37 inject AuthenticationHelper bean 2026-04-17 17:08:00 -06:00
b8e6001089 standardize CRUD services 2026-04-17 16:40:00 -06:00
47fcf082d6 centralize StringUtils usage 2026-04-17 16:15:00 -06:00
9fc51c511d clean remaining code smells 2026-04-17 15:48:00 -06:00
f3ed5e0b9a fix review findings 2026-04-17 15:12:00 -06:00
ced651db32 externalize business constants 2026-04-17 14:43:00 -06:00
a86aa91c16 consolidate shared constants 2026-04-17 14:18:00 -06:00
84b6bac819 use shared StringUtils.trimToNull 2026-04-17 13:54:00 -06:00
b17ca4fbbd unify error handling 2026-04-17 13:31:00 -06:00
31a204fdab fix tests and silent failures 2026-04-17 13:02:00 -06:00
274986186f fix appointment cancellation 2026-04-16 09:45:21 -06:00
4e672859c3 hide chat history button 2026-04-16 09:05:28 -06:00
833e4f5783 fix navbar clipping 2026-04-16 08:57:24 -06:00
d4c7b3469e fix chat session leak 2026-04-16 08:40:38 -06:00
f03df0c4c6 redeploy 2026-04-16 08:25:08 -06:00
ef0ea93514 Merge pull request #322 from RecentRunner/android-desktop-parity
fix six app bugs
2026-04-16 08:13:06 -06:00
c6e38c3972 merge main into branch 2026-04-16 08:12:46 -06:00
4b6895ccd9 normalize pet status casing 2026-04-16 07:58:46 -06:00
417b27b909 guard stale init effects 2026-04-16 07:56:57 -06:00
a1ec3e728b fix six app bugs 2026-04-16 07:55:13 -06:00
d4ac4bface Add V9 sales seed 2026-04-16 07:23:04 -06:00
eaa519dd69 ai greets first with full context 2026-04-16 00:49:58 -06:00
4d6166a882 add order history to profile 2026-04-16 00:38:10 -06:00
26791de867 Seed activity logs, fix role filter 2026-04-16 00:34:13 -06:00
eb7d144b91 fix chat scroll behaviour 2026-04-16 00:25:32 -06:00
3c4ec5b11e fix chat ux and ai model 2026-04-16 00:11:08 -06:00
102edbdb19 fix contact form and appt ui 2026-04-15 23:37:16 -06:00
0bf2582e5c Merge pull request #320 from RecentRunner/fix-desktop-launch
Desktop fixes
2026-04-15 23:21:28 -06:00
83e268d6d4 restore activity logs endpoint 2026-04-15 23:19:22 -06:00
e193e29499 fix contact layout and chat ui 2026-04-15 23:12:23 -06:00
703402b5b6 fix chat badge on reply 2026-04-15 23:11:53 -06:00
c0be2a6903 pet owner search 2026-04-15 23:03:54 -06:00
8a9e4e75b5 fix about section spacing and text 2026-04-15 22:59:18 -06:00
2fecdca917 fix adopt search mobile layout 2026-04-15 22:51:39 -06:00
3ff7df6f88 fix chat escalation and sidebar 2026-04-15 22:46:49 -06:00
7ce22c4801 fix images and empty space 2026-04-15 22:37:51 -06:00
a675bd1d9e fix nav and pagination 2026-04-15 22:30:30 -06:00
c12e7fb8c3 fix layout and demo payment 2026-04-15 22:22:18 -06:00
deff20c057 mobile layout fixes (#319) 2026-04-15 22:01:49 -06:00
0128401486 fix mobile nav and env examples 2026-04-15 21:34:15 -06:00
437baf7ba5 fix stripe publishable key 2026-04-15 21:21:37 -06:00
895ba8e5fd Merge pull request #318 from RecentRunner/fix-customer-ws-subscription
fix customer ws subscription
2026-04-15 21:19:58 -06:00
4674a2eaaf fix customer ws subscription 2026-04-15 20:57:50 -06:00
00459a06b2 fix CORS for production 2026-04-15 18:39:00 -06:00
57d13504e1 fix websocket backend url 2026-04-15 18:32:38 -06:00
56ba097ef4 disable server compression 2026-04-15 18:20:18 -06:00
2d7931f3ec remove incompatible jackson config 2026-04-15 18:12:16 -06:00
a8154e32e4 fix JPQL pet query field 2026-04-15 18:01:23 -06:00
98287ad438 fix duplicate spring key 2026-04-15 17:25:31 -06:00
c46e6820c7 replace chat polling with websocket 2026-04-15 17:04:23 -06:00
34b78bf6c3 fix UserServiceTest constructor 2026-04-15 16:33:14 -06:00
381f75e6a9 force revision on deploy 2026-04-15 16:31:22 -06:00
55b2d76cbb fix yaml and swagger defaults 2026-04-15 16:31:22 -06:00
fe3731d4cf perf: azure deployment optimizations 2026-04-15 16:31:22 -06:00
d09b0a34f1 species service validation (#317)
* fix species-service validation

* add grooming for hamster, other

* expand reptile and other services
2026-04-15 16:25:14 -06:00
b835770cb6 hardcode stripe key 2026-04-15 16:24:58 -06:00
56b3e9932e exclude next cache from build 2026-04-15 16:21:27 -06:00
6c8ea1c993 fix stripe key 2026-04-15 16:17:51 -06:00
f48222bcda Merge pull request #316 from RecentRunner/android-desktop-parity
add pet image support
2026-04-15 16:16:14 -06:00
77793071ce add pet image support 2026-04-15 16:13:50 -06:00
7552bcf1f4 fix stripe key 2026-04-15 16:10:38 -06:00
ebbe46fc10 fix logo LCP 2026-04-15 16:09:07 -06:00
603d88c527 fix nav and color theme 2026-04-15 16:09:07 -06:00
01017a9ae7 Merge pull request #314 from RecentRunner/bug-fixes
Backend bug fixes
2026-04-15 16:08:23 -06:00
5a59c2b1e9 avatar on staff register (#315) 2026-04-15 16:06:17 -06:00
33ef68f27a decouple emails from transactions 2026-04-15 16:03:10 -06:00
cbf9c27bf3 Merge pull request #313 from RecentRunner/worktree-fix-refund-idempotency
lock all stateful mutations
2026-04-15 16:02:37 -06:00
1da991d76d lock all stateful mutations 2026-04-15 16:01:32 -06:00
b65868b4d5 user avatar in edit dialogs (#312) 2026-04-15 15:58:46 -06:00
2031ecc99c fix validation and 500 bugs 2026-04-15 15:58:34 -06:00
8b9c4b899f fix auth and logic bugs 2026-04-15 15:54:46 -06:00
b4d31b13af Merge branch 'loyalty-points' 2026-04-15 15:52:13 -06:00
65bfa1d06f rebuild stripe key 2026-04-15 15:51:06 -06:00
8b473c19f8 fix loyalty points display 2026-04-15 15:49:47 -06:00
f95e1e310d lock refunds against duplicates 2026-04-15 15:48:17 -06:00
be07381bc0 fix sale and adoption bugs 2026-04-15 15:46:46 -06:00
f3d2431dfb center navbar links (#311) 2026-04-15 15:44:45 -06:00
8261c1c750 Merge pull request #310 from RecentRunner/worktree-fix-cart-sessions
restore cart across devices
2026-04-15 15:43:42 -06:00
5a6a63eef6 rebuild stripe key 2026-04-15 15:42:59 -06:00
f1671aef2c restore cart across devices 2026-04-15 15:42:55 -06:00
4f564acd2b rebuild stripe key 2026-04-15 15:38:28 -06:00
fbf69f23dc fix stripe payment intent 2026-04-15 15:31:52 -06:00
8f8959db12 rebuild frontend 2026-04-15 15:10:22 -06:00
0ea6f1d6e3 disable validate on migrate 2026-04-15 14:58:19 -06:00
21a6c26d08 fix flyway failed migration 2026-04-15 14:52:05 -06:00
eec2dce818 update actions node 24 2026-04-15 14:24:39 -06:00
44294ea2ce fix test compilation 2026-04-15 14:20:47 -06:00
6555624b81 fix coupon analytics 2026-04-15 14:10:24 -06:00
654015103b ignore log files 2026-04-15 14:10:24 -06:00
27baaa08e9 remove log artifacts 2026-04-15 14:10:24 -06:00
1cb6caafcf activity logs to files 2026-04-15 14:10:24 -06:00
9f11f04fcf trim seed data 2026-04-15 14:10:14 -06:00
3bd1fb41ff add seed data 2026-04-15 14:10:14 -06:00
1146d3c768 fix scroll sorting 2026-04-15 14:10:14 -06:00
762f01ae71 fix deploy commands 2026-04-15 12:40:15 -06:00
64ce8ffba7 fix CI azure login 2026-04-15 12:36:07 -06:00
ea7e7e3825 Merge pull request #309 from RecentRunner/websitefinal
merge websitefinal
2026-04-15 12:26:47 -06:00
ea4b2292e8 merge main into websitefinal 2026-04-15 12:26:31 -06:00
01660ae4a1 Merge pull request #308 from RecentRunner/azure-deploy
merge azure-deploy
2026-04-15 12:25:46 -06:00
6bce625b12 point android app at azure backend 2026-04-15 08:13:53 -06:00
Nikitha
2fa7b99966 Chat Widget, changes in nav bar
Auto Scroll chat and changes in Nav bar
2026-04-15 08:12:16 -06:00
a22e47eabc point desktop app at azure backend 2026-04-15 08:10:17 -06:00
50f8606ba8 fix chat scroll jumps and move about us to home page 2026-04-15 08:07:17 -06:00
bbda43e4a0 fix proxy origin header and cors allowed origins config 2026-04-15 07:37:43 -06:00
5c85c43d5e Merge pull request #307 from RecentRunner/web-v2
merge web-v2
2026-04-15 07:07:34 -06:00
17ae8f95c0 Merge pull request #306 from RecentRunner/fix-appointment-history
fix appointment history
2026-04-15 07:04:46 -06:00
c18851fba8 fix appointment history 2026-04-15 07:04:22 -06:00
824fea942f Merge pull request #305 from RecentRunner/web-fixes
fix appointments and pagination
2026-04-15 06:53:09 -06:00
0a309598fc fix appointments and pagination 2026-04-15 06:52:49 -06:00
augmentedpotato
c37650d942 Small corrections 2026-04-15 06:39:41 -06:00
augmentedpotato
78a8992f71 Minor change to appointments page 2026-04-15 02:49:26 -06:00
augmentedpotato
dfc3d627c8 Points now subtract from costs 2026-04-15 02:44:14 -06:00
Alex
10ddb4ffdd Merge branch 'AttachmentsToChat' 2026-04-15 02:15:16 -06:00
Alex
7f5b0cceb1 fixed product supplier android 2026-04-15 02:14:20 -06:00
Alex
73b99b4aa0 added filtering to activity logs for desktop 2026-04-15 02:10:20 -06:00
Alex
0cf7144387 turned logs to laymen terms and added to android 2026-04-15 01:52:35 -06:00
47f60dc309 Merge pull request #304 from RecentRunner/web-coupons
web coupons
2026-04-15 01:39:43 -06:00
cae0f23522 Merge pull request #303 from RecentRunner/chat-ui-updates
chat UI updates
2026-04-15 01:37:52 -06:00
b029500553 unify ai and live chat 2026-04-15 01:35:54 -06:00
augmentedpotato
da115fd824 loyalty points 2026-04-15 01:34:49 -06:00
03c8033540 always show chat sidebar 2026-04-15 01:31:29 -06:00
d34c8f34ca Merge pull request #302 from RecentRunner/web-features
fix web chat features
2026-04-15 01:25:47 -06:00
638b15fb40 fix web chat features 2026-04-15 01:25:28 -06:00
Alex
7b176fe938 removed status on purchase order android 2026-04-15 01:09:56 -06:00
f9cd2ed758 fix chat scroll and button 2026-04-15 01:09:09 -06:00
873c1bbb38 fix contact form style 2026-04-15 00:56:24 -06:00
db756d95c4 add store images 2026-04-15 00:55:08 -06:00
augmentedpotato
3f5add59e8 Coupon system working properly 2026-04-15 00:54:46 -06:00
14e4f4ca72 Merge pull request #301 from RecentRunner/web-fixes
web fixes
2026-04-15 00:49:04 -06:00
Alex
6a082fc8f1 added pagenation to android for each fragment 2026-04-15 00:46:32 -06:00
0e1eb056a4 contact form with email 2026-04-15 00:46:32 -06:00
f45330a451 web issue fixes 2026-04-15 00:44:07 -06:00
3111553009 web fixes 2026-04-15 00:38:04 -06:00
51a03fe2c5 Merge branch 'main' of github.com:RecentRunner/group-2-threaded-project-petshop 2026-04-15 00:27:49 -06:00
95d4c84ed8 Merge pull request #300 from RecentRunner/web-pwreset
web password reset
2026-04-15 00:27:40 -06:00
Alex
f66eb12729 fixed issue for desktop when sending a file too large 2026-04-15 00:27:06 -06:00
bbecc168a0 Merge branch 'main' of github.com:RecentRunner/group-2-threaded-project-petshop 2026-04-15 00:18:43 -06:00
augmentedpotato
535004bced password reset 2026-04-15 00:17:26 -06:00
6788afc3d2 merge azure-deploy 2026-04-15 00:14:09 -06:00
Alex
3167e2c68b desktop chat now shows images 2026-04-15 00:00:17 -06:00
13ce9a9cc7 document BACKEND_URL swap 2026-04-14 23:46:38 -06:00
1dc87b0c06 add desktop backend config 2026-04-14 23:46:21 -06:00
Alex
5cf5265451 Merge branch 'AttachmentsToChat' 2026-04-14 23:43:39 -06:00
Alex
1114982553 added time stamp and sender name to android chat 2026-04-14 23:42:23 -06:00
Alex
7847bf19cc fixed chat messaging on same account with different devices 2026-04-14 23:34:07 -06:00
e16e7f5f22 Merge remote-tracking branch 'origin/main' into azure-deploy 2026-04-14 23:32:16 -06:00
f940ac4c10 Merge pull request #299 from RecentRunner/web-v1
Merge web-v1 into main
2026-04-14 23:29:51 -06:00
a4beb245a4 merge main 2026-04-14 23:29:46 -06:00
Alex
2151ac56cd Merge branch 'AttachmentsToChat' 2026-04-14 23:20:16 -06:00
ca3e272fd9 lazy init 2026-04-14 23:19:34 -06:00
Alex
c5a59e8de3 made admin analyics able to select store 2026-04-14 23:17:12 -06:00
Alex
1b4a96c923 added personal and store analytics 2026-04-14 23:10:03 -06:00
1b873f7012 Merge remote-tracking branch 'origin/main' into azure-deploy 2026-04-14 23:06:48 -06:00
0c9feefeb5 show error details 2026-04-14 22:56:54 -06:00
Alex
08337145f0 Changed android phone validation 2026-04-14 22:53:42 -06:00
88bf6f8428 runtime backend proxy 2026-04-14 22:52:01 -06:00
a241024ced use middleware for runtime backend proxy 2026-04-14 22:48:00 -06:00
Alex
7baa780c7f added staff and customer images to desktop 2026-04-14 22:43:24 -06:00
Alex
2b4fcfe24c maade it so sales display points earned 2026-04-14 22:33:10 -06:00
Alex
b7d6adc1cc implemented forget password for desktop 2026-04-14 22:15:15 -06:00
Alex
01cce24997 fixed chat loading issue andriod 2026-04-14 22:08:14 -06:00
Nikitha
f57fe9e233 Chat Saving
saving chat history
2026-04-14 22:02:05 -06:00
2a2c9ac707 fix lowercase image name in workflow 2026-04-14 21:37:16 -06:00
ae8b58a82a trigger CI on azure-deploy branch 2026-04-14 21:35:36 -06:00
316f6f45ed Azure deployment setup 2026-04-14 21:29:00 -06:00
Alex
49cf88f790 Merge branch 'AttachmentsToChat' 2026-04-14 21:15:19 -06:00
Alex
d9fca5b72d added forget password 2026-04-14 21:13:57 -06:00
aa9f3ad444 Merge pull request #296 from RecentRunner/easy-fixes
Logs folder and activity log defaults
2026-04-14 20:50:14 -06:00
92e5919c23 Logs folder and activity log date default 2026-04-14 20:50:03 -06:00
572461b179 Merge pull request #280 from RecentRunner/chat-fixes
Fix chat attachments and avatars
2026-04-14 20:26:03 -06:00
42fb086d02 Fix chat attachments and avatars 2026-04-14 20:25:54 -06:00
413cbae2bc Merge pull request #278 from RecentRunner/desktop-backend-fixes
Activity log, staff role, chat
2026-04-14 20:17:21 -06:00
5420fb3c9e Activity log filters, staff role, chat fix 2026-04-14 20:17:03 -06:00
41b840da96 Merge pull request #275 from RecentRunner/backend-fixes
Backend bug fixes
2026-04-14 20:03:32 -06:00
c00afd2256 Block chat injection 2026-04-14 20:03:11 -06:00
7db8e966fc Add species filtering 2026-04-14 20:03:11 -06:00
f68c23b209 Drop status column 2026-04-14 20:03:11 -06:00
2f4477d28a Fix stuck pet status 2026-04-14 20:03:11 -06:00
677959e3e6 Fix backend issues 2026-04-14 20:03:11 -06:00
d99e476860 Merge pull request #274 from RecentRunner/desktop-notifications
desktop chat notifications
2026-04-14 19:59:57 -06:00
52a3b2cd3b add desktop chat notifications 2026-04-14 19:59:42 -06:00
dcf41675e4 fix backend issues 2026-04-14 19:59:39 -06:00
5c2a29cc91 Merge pull request #268 from RecentRunner/fix/admin-account-guard
Harden admin guards
2026-04-14 16:10:10 -06:00
7194ba12e7 Harden admin guards 2026-04-14 16:09:39 -06:00
d19d633ef0 use openrouter/free model 2026-04-14 15:46:42 -06:00
4164e6ff21 Merge pull request #267 from RecentRunner/nullable-appointment-pet
nullable appointment pet
2026-04-14 15:41:18 -06:00
a8389d00be nullable petId in appointment 2026-04-14 15:39:30 -06:00
ffa84949ef Merge pull request #266 from RecentRunner/resend-email
resend email
2026-04-14 15:23:49 -06:00
d43942fb76 add rate limiting 2026-04-14 15:23:26 -06:00
0411e4be06 add email flows 2026-04-14 15:23:07 -06:00
augmentedpotato
2282f0da6f Profile image works, editing profile works, now uses first/last name 2026-04-14 15:02:57 -06:00
augmentedpotato
c5448b95c9 Age input when editing/adding pet profile 2026-04-14 13:56:21 -06:00
augmentedpotato
5dbddfdc1f Navbar fixed 2026-04-14 13:44:13 -06:00
augmentedpotato
2d27f95f7d Removed addresses, adjusted contact page 2026-04-14 13:31:56 -06:00
augmentedpotato
deccb27213 Fixes for appointments and My Pets fields. 2026-04-14 12:20:48 -06:00
7281691448 Updated the example environment file 2026-04-14 10:02:02 -06:00
augmentedpotato
505560e7da Favicon updated 2026-04-14 07:35:06 -06:00
augmentedpotato
580813792a Pet adoption appointments (currently has a small issue) 2026-04-14 07:14:54 -06:00
8f704eda15 Merge pull request #254 from RecentRunner/web-v1
Web v1
2026-04-14 07:06:21 -06:00
augmentedpotato
995088ece2 Chat now present in the bottom right. 2026-04-14 05:59:33 -06:00
augmentedpotato
4c3c11995a Cart fixes (backend), adjusted header, added footer, mobile formatting updates 2026-04-14 05:24:40 -06:00
Alex
582918e9e1 added filter to analytics desktop 2026-04-14 04:39:38 -06:00
Alex
62434020e2 added filters to desktop 2026-04-14 04:29:28 -06:00
Alex
27e570156c added filter by customer for sales to backend and android 2026-04-14 04:03:15 -06:00
Alex
0b2c7bda13 added clendar to adoptions and appointments on desktop 2026-04-14 03:48:46 -06:00
Alex
5c57cabb4f updated sales on desktop, and fixed sales with points again on back end 2026-04-14 03:32:29 -06:00
Alex
9c2300ad11 Merge branch 'main' into AttachmentsToChat 2026-04-14 01:10:14 -06:00
Alex
1f86158cef Merge branch 'main' into AttachmentsToChat 2026-04-14 00:57:16 -06:00
Alex
98bd619ba6 fixes to desktop part 1 2026-04-14 00:55:51 -06:00
5bd1933ea6 add webp support desktop 2026-04-14 00:17:43 -06:00
a1e930254f consolidate migrations 2026-04-14 00:11:13 -06:00
599104677d fix image paths 2026-04-14 00:11:07 -06:00
f4b62bc8c4 add cart points fields 2026-04-14 00:11:02 -06:00
Alex
6fb7ac7817 Merge branch 'AttachmentsToChat' 2026-04-13 22:36:53 -06:00
Alex
3b07eeaee2 seperated staff and customer on desktop 2026-04-13 22:15:41 -06:00
adc8c477c2 Merge pull request #253 from RecentRunner/images
seed images
2026-04-13 21:58:01 -06:00
f7480f5e80 localize seed image URLs to upload paths 2026-04-13 21:57:29 -06:00
Alex
52624118f6 fixed phone validation desktop 2026-04-13 21:52:53 -06:00
Alex
819109893c added dropdowns for breed desktop 2026-04-13 21:40:34 -06:00
Alex
954154e99c fixed pet busniss logic desktop 2026-04-13 21:10:35 -06:00
Alex
83507f4207 made it so staff cannot change the status of pets for desktop for adopted or owned 2026-04-13 20:49:22 -06:00
a04cdd60d6 Merge pull request #252 from RecentRunner/merge-migration
merge migration
2026-04-13 19:48:37 -06:00
e0297dff6a make ActivityLog entity immutable 2026-04-13 19:47:45 -06:00
Alex
0f65d6c242 added correct refund logic and points for sales 2026-04-13 19:46:52 -06:00
Alex
f13bb90aa0 added points to sale and logic backend 2026-04-13 19:46:28 -06:00
Alex
9dad410c54 added loyaltypoint usage to sales unfinished still needs to work with the backend 2026-04-13 18:52:07 -06:00
1b1236bfb2 seed normalization 2026-04-13 18:29:49 -06:00
Alex
40955b616b Can now edit loyalty points for customer on andriod, and pets now have breed dropdown 2026-04-13 18:25:11 -06:00
af5e3a88ea Merge pull request #251 from RecentRunner/payment-fixes
Payment safety fixes
2026-04-13 17:52:21 -06:00
b64b426406 Unique sale constraint 2026-04-13 17:49:59 -06:00
a5a1757af7 Add payment features 2026-04-13 17:49:59 -06:00
141ca34ea0 Add checkout snapshot 2026-04-13 17:49:59 -06:00
Alex
53a11449ac made sales readonly for andriod 2026-04-13 17:25:28 -06:00
Alex
cba39e8a3c Fixed phone validation for andriod 2026-04-13 17:18:46 -06:00
Alex
688a04c3bc added pending status on pets andiord, also made pets automatically switch to pending when an adoption is in pending 2026-04-13 17:12:41 -06:00
Alex
9fdb3087da Merge branch 'AttachmentsToChat' 2026-04-13 15:07:28 -06:00
Alex
facf1d588b added store column to desktop and display only logged in data 2026-04-13 15:06:04 -06:00
3000a64be3 Merge pull request #172 from RecentRunner/web-adopt-filter
Adopt page filter
2026-04-13 11:08:41 -06:00
de58b0b131 Remove XML declaration from misc.xml 2026-04-13 11:08:23 -06:00
augmentedpotato
23125418c3 Adopt page filter added 2026-04-13 10:34:26 -06:00
Alex
d9c34abcf8 fixed rotated image for pets and product as well 2026-04-13 00:30:26 -06:00
Alex
296f99900a Fix profile image squish and rotate isusse 2026-04-13 00:07:58 -06:00
Alex
227eb9cac8 added coupons to desktop app 2026-04-12 23:47:22 -06:00
Alex
1f801d7486 desktop: added confirmation to change onwer and only admins can change this 2026-04-12 20:01:06 -06:00
Alex
5f7f40f98a added closed chat section and fixed closed chat bug for desktop 2026-04-12 19:49:12 -06:00
Alex
d870475bc9 Fixed minor bugs
- in sales we can no longer select 0 product for a sale
- ActivityLogFragment is locked to admin
- Spinners are loaded aftrer a selection if the spinner depends on a parent spinner
2026-04-12 18:34:43 -06:00
Alex
077780c0c3 adjusted so only available pets for the selected store is displayed when adopting 2026-04-12 18:00:24 -06:00
Alex
42c9e96500 Modified project to use our utils on areas we arnt to manage code 2026-04-12 17:49:24 -06:00
Alex
e6e8dc1b23 Fixed Log filters and fixed chat attachment download 2026-04-12 17:28:40 -06:00
Alex
7a4c711e7f added Activitylogs andriod 2026-04-12 16:58:12 -06:00
Alex
0e9bbcbcea Merge branch 'AttachmentsToChat' 2026-04-12 01:09:25 -06:00
Alex
30f14a5152 added Forgetpassword page with no logic yet 2026-04-12 01:08:25 -06:00
Alex
a6d1c089ac Faded text for disabled spinners in staff 2026-04-12 00:51:49 -06:00
Alex
0df8212a65 staff cant change status or store for pets 2026-04-12 00:49:15 -06:00
Alex
16c832bd14 Made it so only admins can change pet owners 2026-04-12 00:37:15 -06:00
Alex
57e5b06666 Fixed Coupon to use in sales 2026-04-12 00:25:33 -06:00
Alex
54139e9e13 Used the wrong endpoint for populating species, changed to to the correct one
also added coupon option to sales
2026-04-11 23:50:22 -06:00
7f7b9d2ed7 Merge pull request #171 from RecentRunner/admin-activity-logs
Add activity logging
2026-04-11 23:32:38 -06:00
a3e1f67779 restrict activity logging to admin and staff only 2026-04-11 23:29:23 -06:00
79b4f7a3e8 Harden startup config 2026-04-11 23:10:18 -06:00
Alex
2075d0ba17 added petspecies spinner 2026-04-11 23:04:14 -06:00
Alex
9ee3436aa3 Made it so staffs can only manage their own store and they cannot see other branches data 2026-04-11 23:02:52 -06:00
299462d231 Consolidate log migrations 2026-04-11 22:54:43 -06:00
93fed57d4b Add log viewer 2026-04-11 22:54:27 -06:00
933db5304f Add activity logging 2026-04-11 22:54:23 -06:00
Alex
c503ddbbc2 added sort so appointment and adoption display the most recent created first 2026-04-11 22:18:39 -06:00
Alex
6245fa88da fixed bug on appointments where spinners was not populating the correct data 2026-04-11 22:02:06 -06:00
Alex
e7a4e2be7a replaced observer in viewmodels to observe only once to fix memleek 2026-04-11 15:46:30 -06:00
Alex
b69d1c8ce9 Added null checks for Appointment and Adoptions to make sure the spinner can load 2026-04-10 19:49:54 -06:00
Alex
6ee60164ce Added Customer CRUD 2026-04-10 19:34:28 -06:00
c11790c8b2 Merge pull request #170 from RecentRunner/fix-web-pr
Fix web
2026-04-10 12:52:44 -06:00
2db5532078 Fix web 2026-04-10 09:17:31 -06:00
acd52a5fa3 Update postman tests 2026-04-10 09:01:09 -06:00
94c9d65ee9 Fix collection 2026-04-10 08:58:44 -06:00
1a25681f36 Ignore .env files 2026-04-10 08:58:44 -06:00
9b6b04796d Add close chat and closed status 2026-04-10 08:58:44 -06:00
1cc6381e9b Sync postman 2026-04-10 08:58:44 -06:00
865cfb7bc4 Merge pull request #168 from RecentRunner/ai-chat-merge
Add AI chat
2026-04-10 08:58:23 -06:00
bd717c388e Fix duplicate openrouter config 2026-04-10 08:28:43 -06:00
d412490706 Fix sales UI 2026-04-10 08:20:25 -06:00
588ddd7fec Clean up OpenRouterService 2026-04-10 08:19:24 -06:00
973ee4c1d0 Update bot model 2026-04-10 08:18:54 -06:00
c0c984d82e Fix bot runtime 2026-04-10 08:18:54 -06:00
998f476319 OpenRouter bot fixes 2026-04-10 08:18:54 -06:00
1d2f5eab2f Add OpenRouter bot 2026-04-10 08:18:54 -06:00
augmentedpotato
50150b22b7 Web and AI chat 2026-04-10 08:18:54 -06:00
Alex
9d1ccb8e68 Merge branch 'AttachmentsToChat' 2026-04-10 07:43:24 -06:00
Alex
8fc6f4b8d1 added close chat option to chat 2026-04-10 07:36:54 -06:00
Alex
5850adedc3 fixed staff accounts and added coupons andriod 2026-04-10 07:17:19 -06:00
Alex
32e41397d4 Sales bug fix 2026-04-10 05:56:05 -06:00
Alex
9d7c577f85 added Analytics filter 2026-04-10 05:03:36 -06:00
Alex
49ee40b912 Added so adoption status can be missed and fixed adoption bugs for andriod 2026-04-10 04:31:10 -06:00
Alex
3bb399e6e4 fixed spinners to populate the correct pets in edit mode for adoptions 2026-04-10 02:58:14 -06:00
Alex
5340ddf98b added viewstates to Supplier and Product 2026-04-10 00:28:01 -06:00
Alex
0ee097e82d Fixed profile issue with camera and added viewstate to pet and service 2026-04-09 23:39:34 -06:00
be3763c94f Merge pull request #164 from RecentRunner/implement-chat-notifications
implement chat notifications
2026-04-09 23:36:36 -06:00
da95606f64 Restore Main Attachments 2026-04-09 23:28:14 -06:00
738ad0003b Defer Chat Attachments 2026-04-09 23:28:14 -06:00
801b7dc872 Implement chat features 2026-04-09 23:28:14 -06:00
de4fe97dc1 Merge pull request #160 from RecentRunner/gui-fixes
Refactor user management
2026-04-09 23:23:57 -06:00
ba8573f288 resolve merge conflict 2026-04-09 23:23:39 -06:00
ff97839cb7 fix table column bindings using lambdas 2026-04-09 23:18:14 -06:00
e3eaeb0b99 fix duplicate refresh in StaffAccountsController 2026-04-09 23:16:24 -06:00
ef0f46c621 Merge pull request #163 from RecentRunner/stripe-payment
Stripe Payments
2026-04-09 23:12:14 -06:00
5c8c11b03f move stripe keys to .env 2026-04-09 23:01:41 -06:00
014a70b8fc fix user id getter in completeCheckout 2026-04-09 22:57:00 -06:00
ae0ccfd45b fix stripe payment flow 2026-04-09 22:52:57 -06:00
82c39f3993 Merge pull request #162 from RecentRunner/AttachmentsToChat
Attachments to Chat
2026-04-09 22:34:59 -06:00
augmentedpotato
4d91d8b331 Stripe Payment 2026-04-09 22:27:03 -06:00
5b3961064e Merge pull request #161 from RecentRunner/table-fixes
Merge Table Fixes
2026-04-09 21:49:18 -06:00
Alex
1dd350fc8a cleaning code 2026-04-09 21:16:11 -06:00
ef5054e8e3 Restore Table Layout 2026-04-09 21:12:04 -06:00
Alex
c2faeb06ce made chat more user frendly 2026-04-09 19:47:43 -06:00
Alex
9b4aad0c36 fixed sending message with attachments 2026-04-09 18:55:12 -06:00
793053a621 Space Out Tables 2026-04-09 17:57:01 -06:00
0b25e0422a Add Sales Scroll 2026-04-09 17:39:52 -06:00
Alex
f3932b226d added attachments to chat 2026-04-09 17:39:45 -06:00
82e8e04e55 Shrink Sales View 2026-04-09 17:36:31 -06:00
51d063f95c Format Appointment Times 2026-04-09 17:29:18 -06:00
8b4e39416b Refine Desktop Pricing 2026-04-09 17:29:03 -06:00
1205459e53 Refine GUI Behavior 2026-04-09 15:51:21 -06:00
Alex
3db45bde6c fixed bug again 2026-04-09 15:37:24 -06:00
Alex
bc55580831 bug fix 2026-04-09 15:24:20 -06:00
Alex
75341c93d8 deleted unused viewmodels 2026-04-09 15:17:11 -06:00
Alex
38b830509f refactored viewmodels for listfragments 2026-04-09 14:44:04 -06:00
Alex
863692c058 created viewmodels for detailFragments 2026-04-09 14:17:51 -06:00
Alex
30ff2fe04e Merge branch 'main' into AttachmentsToChat 2026-04-09 13:44:24 -06:00
675bf36908 Refactor user management 2026-04-09 12:28:33 -06:00
39fdf8814a Adjust Sales Layout 2026-04-09 11:49:55 -06:00
3ddc9fdf92 Improve Desktop Tables 2026-04-09 11:49:14 -06:00
0cc160a02c Unify Table Behavior 2026-04-09 11:48:30 -06:00
70c883b01b Merge pull request #158 from RecentRunner/remove-sidebar-emojis
Remove sidebar emojis
2026-04-09 11:11:28 -06:00
0930bfcc9a Remove sidebar emojis 2026-04-09 11:10:59 -06:00
9a764222c4 Merge pull request #156 from RecentRunner/early-fixes
Merge Early Fixes
2026-04-09 10:39:53 -06:00
Alex
4664fe177b fixed spinner infinite loop in appointments 2026-04-09 02:48:55 -06:00
Alex
992da24260 split viewmodels for appointments 2026-04-09 02:07:35 -06:00
Alex
2d1c1f8a46 Moved appointments businiss logic to modelview andriod 2026-04-09 00:55:00 -06:00
Alex
071973e787 small change 2026-04-08 21:30:40 -06:00
Alex
05a19ac7c0 Appointments should be fully user frendly now 2026-04-08 20:14:52 -06:00
Alex
e41c4d41d0 helper class added to enable and disable fields 2026-04-08 19:57:04 -06:00
Alex
9998b31ab4 updated backend so booked appointment automatically changes to completed 2026-04-08 19:16:18 -06:00
Alex
ccb0d0dc14 fixing dropdowns 2026-04-08 18:10:18 -06:00
Alex
1e37f25a7a update dropdowns to use backend dropdown endpoints part 1 2026-04-08 17:34:33 -06:00
Alex
8d5d6f3872 making appointment userfrendly part1 andriod 2026-04-08 16:53:42 -06:00
76f22ae16a Update early fixes 2026-04-08 16:43:50 -06:00
be9f07236a Merge pull request #155 from RecentRunner/AttachmentsToChat
Merged attachments branch
2026-04-08 13:44:13 -06:00
3beb4105ea Merge main branch 2026-04-08 13:43:20 -06:00
e9d5c701f0 Fix adoption dialog 2026-04-08 11:23:02 -06:00
78da8716af Show store dropdown 2026-04-08 11:23:02 -06:00
3b9e1374f3 Update pet dialog 2026-04-08 11:23:02 -06:00
28f9fdcff6 Create adoption sale 2026-04-08 11:23:02 -06:00
0fdd603232 Fix dialog issues 2026-04-08 11:23:02 -06:00
8fb4c82a67 Fix desktop chat 2026-04-08 11:23:02 -06:00
559f3bc343 fix empty desktop lists 2026-04-08 08:18:01 -06:00
9e83d7929b restrict adoption pets 2026-04-08 08:11:20 -06:00
930e561ce0 fix desktop chat 2026-04-08 08:07:17 -06:00
39f4b6bd8a remove debit payments 2026-04-08 07:57:39 -06:00
f2932d80c8 fix desktop forms 2026-04-08 07:55:55 -06:00
2ddf5d3249 fix web appointments 2026-04-08 07:17:48 -06:00
656311f185 Merge branch 'main' into web-more-fixes-for-wednesday 2026-04-08 07:17:26 -06:00
Alex
a76dfe4d9c updated sales to have new backend data 2026-04-08 03:19:16 -06:00
Alex
4ad94a318f converted new fragments to use hilt, MVVM and jetpack nav 2026-04-08 02:22:34 -06:00
Alex
f908169bcf Converted merged fragments to viewbinding 2026-04-08 02:06:07 -06:00
Alex
30ae416ba4 Added hamburger menu helperfunction 2026-04-08 01:50:18 -06:00
Alex
3e01ad07cd added filtering for Sales and added helper method for setting up filtertoggle andriod 2026-04-08 01:32:34 -06:00
Alex
4fc33fedf4 fix minor bugs and UI inconsistancy 2026-04-08 00:22:25 -06:00
augmentedpotato
2469c07fef Can now add pets in the appointments page. 2026-04-07 23:53:48 -06:00
augmentedpotato
83477904be Feature parity with admins and users (also a minor backend change) 2026-04-07 23:23:05 -06:00
augmentedpotato
ffef9243dd Fixed(?) being unable to create appointments on today's date 2026-04-07 22:44:50 -06:00
26f9f8c0d8 fix image error responses 2026-04-07 22:41:32 -06:00
d1e77f8ef3 revert adoption fragment 2026-04-07 22:17:15 -06:00
f54bc906f1 Merge branch 'morefiles' 2026-04-07 21:43:20 -06:00
888591c970 finish android merge wiring 2026-04-07 21:15:31 -06:00
60f344d207 fix backend merge conflicts 2026-04-07 21:13:47 -06:00
507314b7e3 merge origin/main into morefiles, resolve all conflicts 2026-04-07 20:29:54 -06:00
Alex
6eddcc49ec added filter by date for adoptions to backend 2026-04-07 18:34:08 -06:00
Alex
9498128ab1 added helper method for filter spinners to maintain code 2026-04-07 18:12:02 -06:00
Alex
492591752d added adoption search and filter andriod and backend 2026-04-07 18:06:07 -06:00
Alex
31df67ef33 fixed purchase order for android on new backend 2026-04-07 17:31:43 -06:00
Alex
ce5adccdfe Merge branch 'main' into AttachmentsToChat 2026-04-07 16:24:17 -06:00
Alex
01f5efa991 added bulk delete for ProductSupplier, appointments, and adoptions 2026-04-07 16:20:51 -06:00
fa4529e123 fix alias folder ordering and request bodies in Postman collection 2026-04-07 16:16:11 -06:00
2f369c0b17 fix audit report mismatches across backend and android 2026-04-07 16:06:44 -06:00
Alex
713e919c10 bluk delete added for Service and suppliers on andriod 2026-04-07 15:24:25 -06:00
b423912a9a update postman collection 2026-04-07 15:19:25 -06:00
Alex
93b4ad8c50 added helper class for bulk delete and mad pets have bulk delete 2026-04-07 15:13:15 -06:00
Alex
0813bb4b44 Did the same to inventory 2026-04-07 14:55:43 -06:00
ada9f7fcf9 update web packages 2026-04-07 14:36:20 -06:00
Alex
679c451c04 updard Adoptions in andriod for new backend 2026-04-07 14:35:57 -06:00
Alex
baa143ff00 edited adapters in andriod to use viewbinding 2026-04-07 14:17:24 -06:00
89c706b893 stabilize desktop chat 2026-04-07 09:38:17 -06:00
7980a7b930 fix desktop chat 2026-04-07 09:34:31 -06:00
3d0e7011c1 fix desktop user inventory crud 2026-04-07 09:31:26 -06:00
f0025886e9 fix desktop adoption save 2026-04-07 09:27:34 -06:00
0cb2ecff02 add my pets api 2026-04-07 09:15:01 -06:00
4d244cc1c5 fix web registration 2026-04-07 09:10:11 -06:00
d3563e1f75 fix desktop appointments 2026-04-07 09:05:08 -06:00
Nikitha
d3b9d28513 loading employee in appointments and adoptions
changes in backend and android
2026-04-07 08:43:49 -06:00
dee517593c Merge pull request #146 from RecentRunner/AttachmentsToChat
AttachmentsToChat
2026-04-07 08:23:47 -06:00
e497abb09d merge main 2026-04-07 08:23:23 -06:00
98a589ec33 update postman collection 2026-04-07 08:17:41 -06:00
Alex
8261cdfc2d added an api connection to Users in Andriod
NOTE Will have to change backend so staffs can access other staffs
2026-04-07 07:46:22 -06:00
Alex
b4c1175ee1 fixed creating adoption for the backend and implemented adoption to andriod for changes 2026-04-07 07:27:37 -06:00
0f0a72455b Merge pull request #145 from RecentRunner/AttachmentsToChat
AttachmentsToChat
2026-04-07 06:53:07 -06:00
Alex
0a55014f21 added my appointments button for logged in user on andriod 2026-04-07 06:48:36 -06:00
Alex
094c2d4a48 added filter options to appointments in the backend and andriod 2026-04-07 06:34:28 -06:00
Alex
9bab45f04b updated Appointments on andriod for new backend 2026-04-07 06:14:17 -06:00
Alex
195c4605f0 changed backend so can sortBy productName and added search to productSupplier 2026-04-07 05:48:24 -06:00
Alex
1990022c1e Added filter by store for inventory in back end and added search to inventory 2026-04-07 05:24:25 -06:00
Alex
ef5651d468 updated inventory backend to have filter by store and added more search features to andriod 2026-04-07 05:09:48 -06:00
Alex
863a85472f updated search to call api for supplier 2026-04-07 04:19:51 -06:00
Alex
37bd69c6f1 updated search for service to call api 2026-04-07 04:12:29 -06:00
Alex
d37202edae Updated Filterdropdown design for pets 2026-04-07 03:51:09 -06:00
Alex
0086bb4a5e added more filter options to pets 2026-04-07 03:24:55 -06:00
Alex
6164a8746d changed filtering and search in pets to use api calls 2026-04-07 02:46:00 -06:00
Alex
867322b462 changed petDetailFragment to support new backend 2026-04-07 02:23:58 -06:00
Alex
fdc6f62441 fixed pet DTO and how it interacts with new backend 2026-04-07 00:27:17 -06:00
Alex
12c7384951 Fixed backend missing file issue 2026-04-07 00:10:42 -06:00
Alex
2b097cf4a9 Merge branch 'main' into AttachmentsToChat 2026-04-06 23:27:47 -06:00
Alex
c86fbedd6f Fixed seeding for backend 2026-04-06 23:27:23 -06:00
Nikitha
ff53e4a1ec Employee files
Add, edit employee (staff and admin)
2026-04-06 22:56:40 -06:00
Nikitha
74472976d5 Sale, refund and analytics documents
view sale history  and refund for sales
analytics of sale , employee and store performances
2026-04-06 22:55:45 -06:00
e3ebb93dd2 seed stores and suppliers before products on fresh DB 2026-04-06 21:18:30 -06:00
e1405984c4 Merge pull request #144 from RecentRunner/backend-refactor
backend-refactor
2026-04-06 21:13:48 -06:00
cf338920dd fix local seed: add missing categories and storeId in inventory insert 2026-04-06 21:13:17 -06:00
a6485b1519 Merge pull request #143 from RecentRunner/backend-refactor
Fix sale inventory and switch to port 3306
2026-04-06 21:06:08 -06:00
1187c7bcc1 point to port 3306 Petstoredb 2026-04-06 21:06:01 -06:00
e631ae2953 scope inventory lookup by store on sale 2026-04-06 21:01:20 -06:00
bad16acefb Merge pull request #142 from RecentRunner/backend-refactor
Fix lazy loading
2026-04-06 20:57:44 -06:00
2795511a41 fix lazy loading on me, services, refunds 2026-04-06 20:56:14 -06:00
07c370983d Merge pull request #141 from RecentRunner/backend-refactor
Backend refactor
2026-04-06 20:51:44 -06:00
2420453daa enable Hibernate validation 2026-04-06 20:46:27 -06:00
dac5f8c4a6 add activityLog store FK 2026-04-06 20:39:08 -06:00
6bde4f4e47 add message attachment fields 2026-04-06 20:38:29 -06:00
31a4356d83 add service species collection 2026-04-06 20:34:03 -06:00
a4ed9a7afc add sale channel coupon cart columns 2026-04-06 20:32:05 -06:00
682bd12873 add coupon cart cartItem entities 2026-04-06 20:30:11 -06:00
969fbdfe8b add adoption sourceStore FK 2026-04-06 20:28:31 -06:00
3f6dc132f4 add purchaseOrder store FK 2026-04-06 20:26:42 -06:00
f86cf72dd9 add storeLocation imageUrl 2026-04-06 20:25:33 -06:00
a74e2ac0ef add store dimension to inventory 2026-04-06 20:24:23 -06:00
0482af966e simplify appointment to single pet 2026-04-06 20:22:26 -06:00
2360dc2419 merge customer/employee into users 2026-04-06 20:17:27 -06:00
824ed7e5eb expand User entity fields 2026-04-06 19:49:38 -06:00
24b11e4152 switch to target DB config 2026-04-06 19:47:18 -06:00
3e74cdd25e Add target DB setup 2026-04-06 19:37:15 -06:00
Alex
0a659fad9e Merge branch 'main' into AttachmentsToChat 2026-04-06 16:29:10 -06:00
7ffea449f7 Fix brittle migrations by replacing hardcoded IDs with robust subqueries 2026-04-06 16:25:45 -06:00
Alex
7f62331773 Merge branch 'main' into AttachmentsToChat 2026-04-06 16:13:50 -06:00
038afc415a Merge pull request #139 from RecentRunner/pet-owner-store
Pet owner store
2026-04-06 16:12:09 -06:00
28e53a4379 Seed pets and appointments 2026-04-06 15:50:52 -06:00
b2f291f256 Update pet desktop 2026-04-06 15:50:49 -06:00
0cc4a2bedd Pet owner and store 2026-04-06 15:50:45 -06:00
Alex
7673504d9e Merge branch 'main' into AttachmentsToChat 2026-04-06 15:39:38 -06:00
a66a779bcb Merge pull request #138 from RecentRunner/employee-phase
Employee phase
2026-04-06 13:37:38 -06:00
cd5dd32c73 Update Postman collection 2026-04-06 13:35:01 -06:00
b5b8290131 Fix Flyway migration 2026-04-06 13:35:01 -06:00
Alex
23d765c6b5 changed detailed fragment to fill data from the backend 2026-04-06 03:12:42 -06:00
419e5302f6 Fix availability checks 2026-04-06 01:51:58 -06:00
661c9b006a Add Missed status 2026-04-06 00:39:37 -06:00
9ea5efe44e Fix employee time conflicts 2026-04-06 00:18:49 -06:00
b70afd66aa Allow cross-store staff selection 2026-04-05 23:58:21 -06:00
a3d454e119 Enforce pet ownership rules 2026-04-05 23:35:05 -06:00
Alex
d62113c0f5 Implemented View Binding to reduce code
- project uses view binding now so we don have to do
getViewbyId to refer to the xml
2026-04-05 22:50:25 -06:00
Alex
1137688d60 Edited RetrofitUtils to also call enqueue to reduce code in repository 2026-04-05 21:57:53 -06:00
Alex
b14e318df2 Fixed bug where it navigates back to petprofile after deleting the pet 2026-04-05 21:40:43 -06:00
Alex
3555b3d2a1 Refactored more of the project to MVVM and created helper class RetrofitUtil to reduce redundent code 2026-04-05 21:27:32 -06:00
Alex
c99d9d21f0 Created help class for displaying diolog and removed redundent code 2026-04-05 18:38:46 -06:00
Alex
6d990fbc63 Created Spinner Helper class and removed reducdent code 2026-04-05 18:15:36 -06:00
Alex
e354592c47 remove dead code 2026-04-05 17:48:14 -06:00
Alex
768103cbac Added Helper class and commented most fragments 2026-04-05 17:16:40 -06:00
521537dc8f Enforce staff-only assignments 2026-04-05 16:17:58 -06:00
153ec836cf Restrict assignments to staff 2026-04-05 16:03:29 -06:00
890391f982 Allow admin ownership bypass 2026-04-05 16:01:46 -06:00
1f343f4132 Harden assignment rules 2026-04-05 15:51:11 -06:00
5d95613786 Harden staff assignment 2026-04-05 12:17:37 -06:00
Alex
453cb54f19 Refactored Andriod project to use MVVM structure (Need to apply this so sales too after merge)
- Used MVVM structure so fragments are not doing all the operation from views to data and calls
- organized the structure of our proejct
2026-04-04 23:35:38 -06:00
Alex
44877cd4ad fix photo loading issue on pets and products 2026-04-04 21:21:25 -06:00
Alex
f59624f9c3 integrated Jetpack navigation to project so we dont have to manually code the functionallities of loading to different fragments 2026-04-04 20:08:40 -06:00
Alex
eee724d4f5 fixed retrofit client, but will delete this file after merges
- kept class so nothing will break when merge
- then delete after merge buy making other files use Hilt
2026-04-04 18:23:15 -06:00
Alex
1516b92a6c integrated hilt so we dont have to manually pass context and inject retrofit in andriod 2026-04-04 18:15:05 -06:00
Alex
933a6bff6b Added calendar view to adoptions in andriod 2026-04-04 17:16:44 -06:00
Alex
313ec4a57b Added enter send message and login for andriod feilds 2026-04-04 16:54:29 -06:00
072c9aadea Merge pull request #135 from RecentRunner/clean-demo-branch
Protect appointment visibility
2026-04-04 16:28:34 -06:00
1079abf0c5 Harden appointment dialog 2026-04-04 16:24:09 -06:00
109f967435 Fix appointment ownership 2026-04-04 16:24:09 -06:00
9a110d377f Hide adopted pets 2026-04-04 16:24:09 -06:00
1043ac096f Merge pull request #134 from RecentRunner/main
Update branch
2026-04-04 16:11:26 -06:00
721ec1c5ce Merge pull request #133 from RecentRunner/AttachmentsToChat
Attachments to chat
2026-04-04 16:10:39 -06:00
Alex
5fa9cfd5d6 added calendar view to appointments
- NOTE: may have to change appointments abit after backend is updated
2026-04-03 19:37:43 -06:00
b86d03d399 Merge pull request #108 from RecentRunner/web-more-fixes
Web more fixes
2026-04-03 18:13:18 -06:00
augmentedpotato
63162487b5 Fix profile images 2026-04-03 15:30:43 -06:00
augmentedpotato
99855a6e99 Fix pet sorting 2026-04-03 15:21:15 -06:00
augmentedpotato
781eb48ca9 Fix item loading 2026-04-03 15:07:41 -06:00
augmentedpotato
a76895434d Improve auth flows 2026-04-03 14:52:32 -06:00
augmentedpotato
3ee59521fd Fix web routing 2026-04-03 14:48:24 -06:00
Alex
8401d9ef62 Added images to products for android
- also added the option to delete the images to profile and pets
2026-04-02 19:21:55 -06:00
Alex
0216435221 made it so we can put attachments to chat
- Sending not implemented until backend is complete
2026-04-02 18:23:49 -06:00
6afda8e7b8 Automate reset cleanup 2026-04-01 20:17:30 -06:00
c0f44842f7 Remove duplicate migration 2026-04-01 20:08:39 -06:00
3efb285e17 Integrate refund logic 2026-04-01 19:59:03 -06:00
a45a437d39 Merge pull request #75 from RecentRunner/fix-features-icons-v2
Finalize feature fixes
2026-04-01 19:31:28 -06:00
14e3b89baf Add desktop icons 2026-04-01 18:10:20 -06:00
2ac2ce339f Fix phone formatting 2026-04-01 18:08:37 -06:00
e1f6d8cae2 Apply service logic 2026-04-01 16:57:27 -06:00
3b31ef4020 Merge migration fixes 2026-04-01 16:55:36 -06:00
6f0eab23ee Revert "Merge pull request #55 from RecentRunner/backend-normalize-users-payments"
This reverts commit 5bd836719b, reversing
changes made to f944124972.
2026-03-30 09:58:02 -06:00
2a0f5e760c Fix migration versions 2026-03-30 09:57:46 -06:00
1c8d87ca54 Merge pull request #60 from RecentRunner/web-products
Web products
2026-03-30 09:51:34 -06:00
84c70a1568 Merge remote-tracking branch 'origin/main' into web-products 2026-03-30 09:50:57 -06:00
686bf1793f Merge pull request #59 from RecentRunner/web-index
Web index
2026-03-30 09:48:50 -06:00
86841b035f Merge pull request #58 from RecentRunner/refund-layout-spacing
Refund polish
2026-03-30 09:41:16 -06:00
4ef913dfd0 Fix refund display 2026-03-30 09:40:22 -06:00
a3851871c7 Stabilize refunds 2026-03-30 09:17:22 -06:00
33c9555564 Polish sales tables 2026-03-30 09:16:52 -06:00
augmentedpotato
00c5198c47 Appointments, account stuff, adopt a pet changes 2026-03-30 05:38:15 -06:00
410b68520a Fix android backend url 2026-03-30 00:03:27 -06:00
2d4e1be832 Merge pull request #57 from RecentRunner/staff-self-analytics
Staff analytics
2026-03-29 23:55:33 -06:00
78aac62138 Fix staff analytics 2026-03-29 23:50:31 -06:00
d2a6332633 Show staff analytics 2026-03-29 23:34:52 -06:00
9c7f931df2 Scope staff analytics 2026-03-29 23:34:43 -06:00
7720fb6c34 Merge remote-tracking branch 'origin/FixedUIConsistancy' 2026-03-29 23:22:43 -06:00
4a68c99c4c Merge pull request #56 from RecentRunner/expand-pets-products-data
Expand catalog
2026-03-29 23:08:37 -06:00
b18599c280 Tighten seed filters 2026-03-29 23:07:16 -06:00
d5fdee10d5 Add pet product filters 2026-03-29 22:54:25 -06:00
01550bac30 Expand pet product data 2026-03-29 22:54:16 -06:00
5bd836719b Merge pull request #55 from RecentRunner/backend-normalize-users-payments
Normalize users
2026-03-29 22:40:13 -06:00
2cacf1f852 Clean up customer accounts 2026-03-29 22:37:18 -06:00
896f500552 Preserve backfill emails 2026-03-29 22:14:53 -06:00
b5efed880d Disable generated user accounts 2026-03-29 22:09:39 -06:00
edbaabb42b Tighten backfill migration 2026-03-29 22:02:44 -06:00
14ca0d8809 Tighten user linking 2026-03-29 21:59:43 -06:00
909026143d Fix user linking 2026-03-29 21:52:45 -06:00
Alex
67f77f4b19 added pet images to petfragment and changed other views to look consistant 2026-03-29 21:47:49 -06:00
0c173060a8 Remove debit payment data 2026-03-29 21:44:10 -06:00
d8622df318 Backfill user accounts 2026-03-29 21:44:06 -06:00
f944124972 Merge pull request #54 from RecentRunner/backend-fixes-41-49
Fix backend appointments and chat
2026-03-29 21:24:36 -06:00
5d490d7d05 Remove chat close wrapper 2026-03-29 21:14:53 -06:00
72b423c8ad Add appointment tests 2026-03-29 21:07:14 -06:00
36ac309442 Update chat conversation status 2026-03-29 21:07:10 -06:00
3b84eff536 Fix appointment overlap rules 2026-03-29 19:02:19 -06:00
ab97a86977 Add chat close endpoint 2026-03-29 19:02:12 -06:00
c37a2fdf45 Merge pull request #53 from RecentRunner/nomorebreaking
Merging in Nikitha's work
2026-03-29 18:21:56 -06:00
c397bfcad1 Fix status bar and navigation bar layout 2026-03-29 18:20:10 -06:00
d66508137b Fix Android app connection and timeout issues
Add proper timeout configuration to OkHttpClient (30s connect/read/write)
Update OkHttp logging-interceptor to 4.12.0 to match OkHttp version
Improve error messages to show server URL for debugging
Configure backend to listen on all interfaces (0.0.0.0)
Remove EdgeToEdge calls that interfered with layout
2026-03-29 17:54:23 -06:00
augmentedpotato
4dd57e3484 Merge branch 'web-index' 2026-03-29 17:47:33 -06:00
55caf434d6 Merge main into nomorebreaking 2026-03-29 17:07:35 -06:00
fdf438f3dd Merge pull request #52 from RecentRunner/desktop-pet-product-pictures
Add desktop pet and product images
2026-03-29 16:58:06 -06:00
Nikitha
324e5d4e17 sales files
basic sale view and builtfiles
2026-03-29 16:54:41 -06:00
93e71434df fix desktop pet and product dialogs 2026-03-29 16:54:01 -06:00
Nikitha
c4775bcbfa Backend a small change
small change in appointment service to validate time for appointments.
2026-03-29 16:35:18 -06:00
Nikitha
862ece691a purchaseorder view
Purchase Order view only so changes only to view
2026-03-29 16:33:27 -06:00
Nikitha
13916236eb connected files
files connected to load
2026-03-29 16:32:25 -06:00
Nikitha
2ad097413b Product files
loads details of product, categories and cost of it
2026-03-29 16:29:40 -06:00
Nikitha
653ae3f233 ProductSupplier
prodcutSupllier -loads all product supplier details and  with cost details
2026-03-29 16:28:19 -06:00
Nikitha
87a4404c20 Inventory
Inventory- details of product loads with id and described with filter, and categories selection
2026-03-29 16:26:21 -06:00
Nikitha
55f40572de Adoption files
adoption
2026-03-29 16:24:14 -06:00
Nikitha
7e832a139f Appointments
validation, time slots for booking appointment , date, store. connected to service Type, pet name, customer name. Loads from database
2026-03-29 16:20:38 -06:00
8c6a53250a add desktop pet and product images 2026-03-27 10:07:37 -06:00
Alex
8272c49b44 Changed android app icon 2026-03-26 22:45:28 -06:00
Alex
38e1a29f34 Added petprofile images and uploads in petprofilefragment 2026-03-26 22:07:51 -06:00
Alex
5d8d37dee4 Merge branch 'MorePushNotification' 2026-03-26 21:39:38 -06:00
Alex
2c61e6e664 Make chat notification display messengers name and disable notifying if already in chat view 2026-03-26 21:31:36 -06:00
2fb409f0d9 add pet and product images 2026-03-26 20:36:04 -06:00
Alex
dbb24085b2 Added push notifications when reciving any message and added filter status on pets in Andriod 2026-03-26 20:13:27 -06:00
Alex
75c39312fe Added profile photo loading and uploading
- profile photos now load from backend
- profile photos can be uploaded to the backend
- RetrofitClient now automatically determines if the device is an emulator or hardware so we dont have to comment and uncomment everytime we test with a different device
2026-03-26 16:50:02 -06:00
Alex
aec9f7b9e0 Added role based access to android login
- Admin has access to everything
- Staff has limited access to what they can edit in listfragment
- Customers cannot login to app
- added validations to pets, supplier and services in their detailed view
2026-03-26 16:50:02 -06:00
5477c4beee use swing picker on wayland 2026-03-25 23:55:40 -06:00
4659aa44df readd secure avatar endpoints 2026-03-25 22:58:04 -06:00
b1fe03410c Merge pull request #30 from RecentRunner/WorkingOnProfileAndPushNotification
Working on profile and push notification
2026-03-25 09:18:48 -06:00
Alex
d3a69b7aea added push notification when a new conversation is made 2026-03-24 22:41:54 -06:00
d3fdf4f823 Merge pull request #29 from RecentRunner/desktop---validator-fixes
added null checks to validator, created a bunch of junit tests
2026-03-24 20:43:54 -06:00
augmentedpotato
dbdf5e54ab added null checks to validator, created a bunch of junit tests 2026-03-24 20:42:45 -06:00
Alex
b46705396d Profile now loads from backend
- can update profile from app
- loads profile details from backend to display
- changed inputValidator to use andriod phone pattern
- added ErrorResponse so we can fetch error messages from the backend
- Added UserDTO to get profile info

TODO:
Still need to get profile images from the backend and beable to load and update them using the backend
2026-03-24 18:31:12 -06:00
b012c91b3b Merge pull request #28 from RecentRunner/web-index
uploading index to repo
2026-03-24 16:59:39 -06:00
982 changed files with 71265 additions and 10537 deletions

114
.github/workflows/deploy.yml vendored Normal file
View 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
View File

@@ -0,0 +1,5 @@
*.zip
.local/
commit-patches/
temp_photos/
.env

10
.idea/.gitignore generated vendored Normal file
View 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

File diff suppressed because it is too large Load Diff

View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

3
android/.gitignore vendored
View File

@@ -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

View File

@@ -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>

View File

@@ -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
View 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>

View File

@@ -1,3 +1,7 @@
/build
/nohup.out
/.classpath
/.project
/.settings/
/src/test/
/src/androidTest/

View File

@@ -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)

View File

@@ -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">

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -1,13 +1,12 @@
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();
}
}

View File

@@ -0,0 +1,84 @@
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;
@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());
}
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();
}
});
}
}

View File

@@ -1,69 +1,115 @@
package com.example.petstoremobile.activities;
import android.Manifest;
import android.content.Intent;
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.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;
// 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;
});
// Start the notification service and request for notification permission
startNotificationService();
requestNotificationPermission();
}
//helper function to load a fragment
private void loadFragment(Fragment fragment) {
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.fragment_container, fragment)
.commit();
/**
* Handles new intents received while the activity is already running (like notifications).
*/
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent); // Set the new intent so fragments can access updated extras
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 POST_NOTIFICATIONS 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);
}
}
}
}

View File

@@ -2,10 +2,7 @@ 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 +10,141 @@ 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));
});
}
/**
* Executes the 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();
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 logged-in 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();
}
});
});
}
}

View File

@@ -0,0 +1,117 @@
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;
public ActivityLogAdapter(List<ActivityLogDTO> items) {
this.items = items;
}
@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);
}
@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()));
}
@Override
public int getItemCount() { return items.size(); }
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;
}
}
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;
}
}
private String firstNonBlank(String... values) {
for (String v : values) {
if (v != null && !v.isBlank()) return v;
}
return "";
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvActivity, tvTechnical, tvUser, tvMeta, tvTimestamp;
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);
}
}
}

View File

@@ -1,79 +1,128 @@
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) {
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();
}
});
}
@Override
public List<String> getSelectedKeys() {
return selectionHelper.getSelectedKeys();
}
@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
@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
@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"));
} else {
holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#F44336"));
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;
}
// When a row is clicked, open the detail view
holder.itemView.setOnClickListener(v -> adoptionClickListener.onAdoptionClick(position));
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 {
binding.cbSelectAdoption.setVisibility(View.GONE);
binding.cbSelectAdoption.setChecked(false);
}
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;
});
}
@Override
public int getItemCount() {
return adoptionList.size();
}
public int getItemCount() { return adoptionList.size(); }
}

View File

@@ -1,82 +1,127 @@
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) {
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);
}
@Override
public void onSelectionModeToggle(boolean selectionMode) {
notifyDataSetChanged();
}
});
}
@Override
public List<String> getSelectedKeys() {
return selectionHelper.getSelectedKeys();
}
@Override
public void clearSelection() {
selectionHelper.clearSelection();
}
// Get the controls of each row in recycler view
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);
public AppointmentViewHolder(@NonNull ItemAppointmentBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
// Create a new row view
@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
@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;
});
}
@Override

View File

@@ -0,0 +1,44 @@
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
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;
}
}

View File

@@ -1,14 +1,12 @@
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;
@@ -30,15 +28,15 @@ public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ChatViewHolder
@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);
}
@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));
}
@@ -48,12 +46,11 @@ public class ChatAdapter extends RecyclerView.Adapter<ChatAdapter.ChatViewHolder
}
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);
public ChatViewHolder(@NonNull ItemChatBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}

View File

@@ -0,0 +1,138 @@
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);
}
public CouponAdapter(List<CouponDTO> coupons, OnCouponClickListener listener) {
this.coupons = coupons;
this.listener = listener;
}
@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);
}
@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()));
}
private void toggleSelection(Long id) {
if (selectedIds.contains(id)) {
selectedIds.remove(id);
} else {
selectedIds.add(id);
}
notifyDataSetChanged();
listener.onSelectionChanged(selectedIds.size());
}
public void setSelectionMode(boolean selectionMode) {
this.selectionMode = selectionMode;
if (!selectionMode) selectedIds.clear();
notifyDataSetChanged();
listener.onSelectionChanged(selectedIds.size());
}
public Set<Long> getSelectedIds() {
return selectedIds;
}
@Override
public int getItemCount() {
return coupons.size();
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView tvCouponCode, tvCouponDiscount, tvCouponMinOrder, tvCouponExpiry, tvCouponStatus;
CheckBox cbSelectCoupon;
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);
}
}
}

View File

@@ -0,0 +1,80 @@
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);
}
public CustomerAdapter(List<CustomerDTO> list, OnCustomerClickListener listener) {
this.list = list;
this.listener = listener;
}
public void setBaseUrl(String baseUrl) { this.baseUrl = baseUrl; }
public void setToken(String token) { this.token = token; }
public static class CustomerViewHolder extends RecyclerView.ViewHolder {
final ItemCustomerBinding binding;
public CustomerViewHolder(@NonNull ItemCustomerBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public CustomerViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemCustomerBinding binding = ItemCustomerBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false);
return new CustomerViewHolder(binding);
}
@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));
}
@Override
public int getItemCount() { return list.size(); }
}

View File

@@ -0,0 +1,95 @@
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);
}
public EmployeeAdapter(List<EmployeeDTO> list, OnEmployeeClickListener listener) {
this.list = list;
this.listener = listener;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setToken(String token) {
this.token = token;
}
public static class EmployeeViewHolder extends RecyclerView.ViewHolder {
private final ItemEmployeeBinding binding;
public EmployeeViewHolder(@NonNull ItemEmployeeBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public EmployeeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemEmployeeBinding binding = ItemEmployeeBinding.inflate(
LayoutInflater.from(parent.getContext()), parent, false);
return new EmployeeViewHolder(binding);
}
@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));
}
@Override
public int getItemCount() { return list.size(); }
}

View File

@@ -1,75 +1,122 @@
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) {
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);
}
@Override
public void onSelectionModeToggle(boolean selectionMode) {
notifyDataSetChanged();
}
});
}
@Override
public List<String> getSelectedKeys() {
return selectionHelper.getSelectedKeys();
}
@Override
public void clearSelection() {
selectionHelper.clearSelection();
}
// Get the controls of each row in recycler view
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);
public InventoryViewHolder(@NonNull ItemInventoryBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
// Create a new row view
@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
@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);
// Low stock = red, normal = green (like desktop reorder concept)
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;
});
}
@Override

View File

@@ -3,24 +3,50 @@ 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;
public MessageAdapter(List<Message> messages, Long currentUserId) {
this.messages = messages;
this.currentUserId = currentUserId;
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
Message m = messages.get(position);
return m.getId() != null ? m.getId() : position;
}
public void setCurrentUserId(Long id) {
@@ -28,6 +54,23 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
notifyDataSetChanged();
}
public void setStaffId(Long id) {
this.staffId = id;
notifyDataSetChanged();
}
public void setToken(String token) {
this.token = token;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setOnAttachmentClickListener(OnAttachmentClickListener listener) {
this.attachmentClickListener = listener;
}
@Override
public int getItemViewType(int position) {
Message m = messages.get(position);
@@ -41,38 +84,151 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
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);
}
}
@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);
}
@Override public int getItemCount() { return messages.size(); }
static class SentHolder extends RecyclerView.ViewHolder {
TextView tvMessage;
SentHolder(View v) {
super(v);
tvMessage = v.findViewById(R.id.tvMessageContent); // updated
final ItemMessageSentBinding binding;
SentHolder(ItemMessageSentBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
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);
}
void bind(Message m) { tvMessage.setText(m.getContent()); }
}
static class ReceivedHolder extends RecyclerView.ViewHolder {
TextView tvMessage;
ReceivedHolder(View v) {
super(v);
tvMessage = v.findViewById(R.id.tvMessageContent); // updated
final ItemMessageReceivedBinding binding;
ReceivedHolder(ItemMessageReceivedBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
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);
}
}
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";
}
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 "";
}
}
private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token, String baseUrl) {
// Check if there's an attachment by looking at name or mime type
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(); // Fallback
}
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 stale cached images for the same URL/ID
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()); }
}
}

View File

@@ -4,40 +4,75 @@ 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
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);
}
@Override
public void onSelectionModeToggle(boolean selectionMode) {
notifyDataSetChanged();
}
});
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setToken(String token) {
this.token = token;
}
@Override
public List<String> getSelectedKeys() {
return selectionHelper.getSelectedKeys();
}
@Override
public void clearSelection() {
selectionHelper.clearSelection();
}
// Get the controls of each row in recycler view
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);
public PetViewHolder(@NonNull ItemPetBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@@ -45,37 +80,81 @@ public class PetAdapter extends RecyclerView.Adapter<PetAdapter.PetViewHolder> {
@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
@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 available, green, If Pending, yellow, otherwise red
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;
});
}
@Override

View File

@@ -1,72 +1,77 @@
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) {
public ProductAdapter(List<ProductDTO> productList, OnProductClickListener listener) {
this.productList = productList;
this.productClickListener = productClickListener;
this.listener = listener;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
public void setToken(String token) {
this.token = token;
}
// Get the controls of each row in recycler view
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);
public ProductViewHolder(@NonNull ItemProductBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
// Create a new row view
@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
@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));
}
@Override
public int getItemCount() {
return productList.size();
public int getItemCount() { return productList.size(); }
}
}

View File

@@ -0,0 +1,109 @@
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);
}
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();
}
});
}
@Override
public List<String> getSelectedKeys() {
return selectionHelper.getSelectedKeys();
}
@Override
public void clearSelection() {
selectionHelper.clearSelection();
}
public static class PSViewHolder extends RecyclerView.ViewHolder {
final ItemProductSupplierBinding binding;
public PSViewHolder(@NonNull ItemProductSupplierBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public PSViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemProductSupplierBinding binding = ItemProductSupplierBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new PSViewHolder(binding);
}
@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(); }
}

View File

@@ -0,0 +1,58 @@
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);
}
public PurchaseOrderAdapter(List<PurchaseOrderDTO> list, OnPurchaseOrderClickListener listener) {
this.list = list;
this.listener = listener;
}
public static class POViewHolder extends RecyclerView.ViewHolder {
final ItemPurchaseOrderBinding binding;
public POViewHolder(@NonNull ItemPurchaseOrderBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public POViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemPurchaseOrderBinding binding = ItemPurchaseOrderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new POViewHolder(binding);
}
@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();
}
}

View File

@@ -0,0 +1,76 @@
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);
}
public SaleAdapter(List<SaleDTO> saleList, OnSaleClickListener listener) {
this.saleList = saleList;
this.listener = listener;
}
public static class SaleViewHolder extends RecyclerView.ViewHolder {
final ItemSaleBinding binding;
public SaleViewHolder(@NonNull ItemSaleBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@NonNull
@Override
public SaleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemSaleBinding binding = ItemSaleBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new SaleViewHolder(binding);
}
@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();
}
}

View File

@@ -3,39 +3,69 @@ 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) {
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
public static class ServiceViewHolder extends RecyclerView.ViewHolder {
TextView tvServiceName, tvServiceDesc, tvServiceDuration, tvServicePrice;
@Override
public void onSelectionModeToggle(boolean selectionMode) {
notifyDataSetChanged();
}
});
}
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);
@Override
public List<String> getSelectedKeys() {
return selectionHelper.getSelectedKeys();
}
@Override
public void clearSelection() {
selectionHelper.clearSelection();
}
/**
* ViewHolder class for service items.
*/
public static class ServiceViewHolder extends RecyclerView.ViewHolder {
final ItemServiceBinding binding;
public ServiceViewHolder(@NonNull ItemServiceBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@@ -43,22 +73,47 @@ public class ServiceAdapter extends RecyclerView.Adapter<ServiceAdapter.ServiceV
@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
@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

View File

@@ -3,39 +3,63 @@ 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
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);
}
@Override
public void onSelectionModeToggle(boolean selectionMode) {
notifyDataSetChanged();
}
});
}
@Override
public List<String> getSelectedKeys() {
return selectionHelper.getSelectedKeys();
}
@Override
public void clearSelection() {
selectionHelper.clearSelection();
}
// Get the controls of each row in recycler view
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);
public SupplierViewHolder(@NonNull ItemSupplierBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
@@ -43,22 +67,48 @@ public class SupplierAdapter extends RecyclerView.Adapter<SupplierAdapter.Suppli
@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
@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

View File

@@ -0,0 +1,47 @@
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 is white and background is transparent.
*/
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;
}
}

View File

@@ -0,0 +1,22 @@
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;
public interface ActivityLogApi {
@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
);
}

View File

@@ -0,0 +1,44 @@
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;
public interface AdoptionApi {
@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("api/v1/adoptions/{id}")
Call<AdoptionDTO> getAdoptionById(@Path("id") Long id);
@POST("api/v1/adoptions")
Call<AdoptionDTO> createAdoption(@Body AdoptionDTO adoption);
@PUT("api/v1/adoptions/{id}")
Call<AdoptionDTO> updateAdoption(@Path("id") Long id, @Body AdoptionDTO adoption);
@DELETE("api/v1/adoptions/{id}")
Call<Void> deleteAdoption(@Path("id") Long id);
@HTTP(method = "DELETE", path = "api/v1/adoptions", hasBody = true)
Call<Void> bulkDeleteAdoptions(@Body BulkDeleteRequest request);
}

View File

@@ -0,0 +1,44 @@
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;
public interface AppointmentApi {
@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("api/v1/appointments/{id}")
Call<AppointmentDTO> getAppointmentById(@Path("id") Long id);
@POST("api/v1/appointments")
Call<AppointmentDTO> createAppointment(@Body AppointmentDTO appointment);
@PUT("api/v1/appointments/{id}")
Call<AppointmentDTO> updateAppointment(@Path("id") Long id, @Body AppointmentDTO appointment);
@DELETE("api/v1/appointments/{id}")
Call<Void> deleteAppointment(@Path("id") Long id);
@HTTP(method = "DELETE", path = "api/v1/appointments", hasBody = true)
Call<Void> bulkDeleteAppointments(@Body BulkDeleteRequest request);
}

View File

@@ -0,0 +1,15 @@
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;
public interface CategoryApi {
@GET("api/v1/categories")
Call<PageResponse<CategoryDTO>> getAllCategories(
@Query("page") int page,
@Query("size") int size);
}

View File

@@ -2,13 +2,14 @@ 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
@@ -20,4 +21,7 @@ public interface ChatApi {
@GET("api/v1/chat/conversations/{conversationId}")
Call<ConversationDTO> getConversationById(@Path("conversationId") Long conversationId);
@PUT("api/v1/chat/conversations/{conversationId}")
Call<ConversationDTO> updateConversationStatus(@Path("conversationId") Long conversationId, @Body UpdateConversationStatusRequest request);
}

View File

@@ -0,0 +1,44 @@
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 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;
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);
@DELETE("api/v1/coupons")
Call<Void> bulkDeleteCoupons(@Query("ids") List<Long> ids);
}

View File

@@ -1,16 +1,20 @@
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
public interface CustomerApi {
@GET("api/v1/customers")
@@ -18,4 +22,16 @@ public interface CustomerApi {
@GET("api/v1/customers/{customerId}")
Call<CustomerDTO> getCustomerById(@Path("customerId") Long customerId);
@PUT("api/v1/customers/{customerId}")
Call<CustomerDTO> updateCustomer(@Path("customerId") Long customerId, @Body CustomerDTO customer);
@DELETE("api/v1/customers/{customerId}")
Call<Void> deleteCustomer(@Path("customerId") Long customerId);
@POST("api/v1/auth/register")
Call<CustomerDTO> registerCustomer(@Body CustomerDTO customer);
@GET("api/v1/dropdowns/customers")
Call<List<DropdownDTO>> getCustomerDropdowns();
}

View File

@@ -0,0 +1,32 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.EmployeeDTO;
import com.example.petstoremobile.dtos.PageResponse;
import retrofit2.Call;
import retrofit2.http.*;
public interface EmployeeApi {
@GET("api/v1/employees")
Call<PageResponse<EmployeeDTO>> getAllEmployees(
@Query("page") int page,
@Query("size") int size);
@GET("api/v1/employees/{id}")
Call<EmployeeDTO> getEmployeeById(@Path("id") Long id);
@POST("api/v1/employees")
Call<EmployeeDTO> createEmployee(@Body EmployeeDTO employee);
@PUT("api/v1/employees/{id}")
Call<EmployeeDTO> updateEmployee(@Path("id") Long id, @Body EmployeeDTO employee);
@DELETE("api/v1/employees/{id}")
Call<Void> deleteEmployee(@Path("id") Long id);
}

View File

@@ -0,0 +1,46 @@
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;
public interface InventoryApi {
@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 /api/v1/inventory/{id}
@GET("api/v1/inventory/{id}")
Call<InventoryDTO> getInventoryById(@Path("id") Long id);
// POST /api/v1/inventory
@POST("api/v1/inventory")
Call<InventoryDTO> createInventory(@Body InventoryDTO request);
// PUT /api/v1/inventory/{id}
@PUT("api/v1/inventory/{id}")
Call<InventoryDTO> updateInventory(@Path("id") Long id, @Body InventoryDTO request);
// DELETE /api/v1/inventory/{id}
@DELETE("api/v1/inventory/{id}")
Call<Void> deleteInventory(@Path("id") Long id);
// DELETE /api/v1/inventory (bulk delete)
@HTTP(method = "DELETE", path = "api/v1/inventory", hasBody = true)
Call<Void> bulkDeleteInventory(@Body BulkDeleteRequest request);
}

View File

@@ -3,11 +3,17 @@ 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
public interface MessageApi {
@@ -17,4 +23,16 @@ public interface MessageApi {
@POST("api/v1/chat/conversations/{id}/messages")
Call<MessageDTO> sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request);
@Multipart
@POST("api/v1/chat/conversations/{id}/attachments")
Call<MessageDTO> sendMessageWithAttachment(
@Path("id") Long conversationId,
@Part MultipartBody.Part content,
@Part MultipartBody.Part file
);
@GET("api/v1/chat/messages/{id}/attachment")
@Streaming
Call<ResponseBody> downloadAttachment(@Path("id") Long messageId);
}

View File

@@ -1,26 +1,58 @@
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("api/v1/dropdowns/customers/{customerId}/pets")
Call<List<DropdownDTO>> getCustomerPets(@Path("customerId") Long customerId);
@GET("api/v1/dropdowns/adoption-pets")
Call<List<DropdownDTO>> getAdoptionPets(@Query("storeId") Long storeId);
@GET("api/v1/dropdowns/pets")
Call<List<DropdownDTO>> getPetDropdowns();
@GET("api/v1/dropdowns/pet-species")
Call<List<DropdownDTO>> getPetSpeciesDropdowns();
@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 +69,17 @@ 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);
}

View File

@@ -0,0 +1,47 @@
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;
public interface ProductApi {
String PRODUCT_IMAGE_PATH = "api/v1/products/%d/image";
@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("api/v1/products/{id}")
Call<ProductDTO> getProductById(@Path("id") Long id);
@POST("api/v1/products")
Call<ProductDTO> createProduct(@Body ProductDTO product);
@PUT("api/v1/products/{id}")
Call<ProductDTO> updateProduct(@Path("id") Long id, @Body ProductDTO product);
@DELETE("api/v1/products/{id}")
Call<Void> deleteProduct(@Path("id") Long id);
@Multipart
@POST("api/v1/products/{id}/image")
Call<Void> uploadProductImage(@Path("id") Long id, @Part MultipartBody.Part image);
@DELETE("api/v1/products/{id}/image")
Call<Void> deleteProductImage(@Path("id") Long id);
@GET("api/v1/dropdowns/products")
Call<List<DropdownDTO>> getProductDropdowns();
@GET("api/v1/dropdowns/categories")
Call<List<DropdownDTO>> getCategoryDropdowns();
}

View File

@@ -0,0 +1,40 @@
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.*;
public interface ProductSupplierApi {
@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("api/v1/product-suppliers/{productId}/{supplierId}")
Call<ProductSupplierDTO> getProductSupplierById(
@Path("productId") Long productId,
@Path("supplierId") Long supplierId);
@POST("api/v1/product-suppliers")
Call<ProductSupplierDTO> createProductSupplier(@Body ProductSupplierDTO dto);
@PUT("api/v1/product-suppliers/{productId}/{supplierId}")
Call<ProductSupplierDTO> updateProductSupplier(
@Path("productId") Long productId,
@Path("supplierId") Long supplierId,
@Body ProductSupplierDTO dto);
@DELETE("api/v1/product-suppliers/{productId}/{supplierId}")
Call<Void> deleteProductSupplier(
@Path("productId") Long productId,
@Path("supplierId") Long supplierId);
@HTTP(method = "DELETE", path = "api/v1/product-suppliers", hasBody = true)
Call<Void> bulkDeleteProductSuppliers(@Body BulkDeleteRequest request);
}

View File

@@ -0,0 +1,22 @@
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;
public interface PurchaseOrderApi {
@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("api/v1/purchase-orders/{id}")
Call<PurchaseOrderDTO> getPurchaseOrderById(@Path("id") Long id);
}

View File

@@ -0,0 +1,25 @@
package com.example.petstoremobile.api;
import com.example.petstoremobile.dtos.RefundDTO;
import retrofit2.Call;
import retrofit2.http.*;
import java.util.List;
public interface RefundApi {
@GET("api/v1/refunds")
Call<List<RefundDTO>> getAllRefunds();
@GET("api/v1/refunds/{id}")
Call<RefundDTO> getRefundById(@Path("id") Long id);
@POST("api/v1/refunds")
Call<RefundDTO> createRefund(@Body RefundDTO refund);
@PUT("api/v1/refunds/{id}")
Call<RefundDTO> updateRefund(@Path("id") Long id, @Body RefundDTO refund);
@DELETE("api/v1/refunds/{id}")
Call<Void> deleteRefund(@Path("id") Long id);
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,31 @@
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;
public interface SaleApi {
@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("api/v1/sales/{id}")
Call<SaleDTO> getSaleById(@Path("id") Long id);
@POST("api/v1/sales")
Call<SaleDTO> createSale(@Body SaleDTO sale);
}

View File

@@ -1,5 +1,6 @@
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 +8,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 +20,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 +40,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);
}

View File

@@ -0,0 +1,26 @@
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;
public interface StoreApi {
@GET("api/v1/stores")
Call<PageResponse<StoreDTO>> getAllStores(
@Query("page") int page,
@Query("size") int size);
@GET("api/v1/dropdowns/stores")
Call<List<DropdownDTO>> getStoreDropdowns();
@GET("api/v1/dropdowns/stores/{storeId}/employees")
Call<List<DropdownDTO>> getStoreEmployees(@Path("storeId") Long storeId);
}

View File

@@ -1,5 +1,6 @@
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 +8,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 +20,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 +40,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);
}

View File

@@ -0,0 +1,15 @@
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;
public interface UserApi {
String AVATAR_PATH = "api/v1/users/%d/avatar/file";
@GET("api/v1/users")
Call<PageResponse<UserDTO>> getUsers(@Query("role") String role, @Query("page") int page, @Query("size") int size);
}

View File

@@ -1,17 +1,50 @@
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);
}

View File

@@ -1,7 +1,5 @@
package com.example.petstoremobile.api.auth;
import android.content.Context;
import androidx.annotation.NonNull;
import java.io.IOException;
@@ -15,8 +13,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

View File

@@ -3,28 +3,27 @@ package com.example.petstoremobile.api.auth;
import android.content.Context;
import android.content.SharedPreferences;
//Store login token in shared preferences
import javax.inject.Inject;
import javax.inject.Singleton;
import dagger.hilt.android.qualifiers.ApplicationContext;
@Singleton
public class TokenManager {
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 SharedPreferences prefs;
private TokenManager(Context context) {
@Inject
public TokenManager(@ApplicationContext 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;
}
//save login data after login
public void saveLoginData(String token, String username, String role) {
prefs.edit()
@@ -56,6 +55,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 +77,4 @@ public class TokenManager {
public void clearLoginData() {
prefs.edit().clear().apply();
}
}

View File

@@ -0,0 +1,206 @@
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);
}
}

View File

@@ -0,0 +1,31 @@
package com.example.petstoremobile.dtos;
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; }
}

View File

@@ -0,0 +1,76 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
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; }
}

View File

@@ -0,0 +1,80 @@
package com.example.petstoremobile.dtos;
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; }
}

View File

@@ -0,0 +1,25 @@
package com.example.petstoremobile.dtos;
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;
}
}

View File

@@ -0,0 +1,22 @@
package com.example.petstoremobile.dtos;
import java.util.List;
public class BulkDeleteRequest {
private List<String> ids;
public BulkDeleteRequest() {
}
public BulkDeleteRequest(List<String> ids) {
this.ids = ids;
}
public List<String> getIds() {
return ids;
}
public void setIds(List<String> ids) {
this.ids = ids;
}
}

View File

@@ -0,0 +1,29 @@
package com.example.petstoremobile.dtos;
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;
}
}

View File

@@ -12,6 +12,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;
}

View File

@@ -0,0 +1,52 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
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; }
}

View File

@@ -3,57 +3,78 @@ package com.example.petstoremobile.dtos;
import com.google.gson.annotations.SerializedName;
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; }
}

View File

@@ -0,0 +1,29 @@
package com.example.petstoremobile.dtos;
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;
}
}

View File

@@ -0,0 +1,156 @@
package com.example.petstoremobile.dtos;
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;
}
}

View File

@@ -0,0 +1,15 @@
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;
}
}

View File

@@ -0,0 +1,96 @@
package com.example.petstoremobile.dtos;
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;
}
}

View File

@@ -22,6 +22,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 +59,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; }
}

View File

@@ -7,9 +7,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 +33,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; }
}

View File

@@ -0,0 +1,81 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
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;
}
}

View File

@@ -0,0 +1,48 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
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;
}
}

View File

@@ -0,0 +1,49 @@
package com.example.petstoremobile.dtos;
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;
}
}

View File

@@ -0,0 +1,58 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
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;
}
}

View File

@@ -0,0 +1,190 @@
package com.example.petstoremobile.dtos;
import java.math.BigDecimal;
import java.util.List;
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;
}
}
}

View File

@@ -0,0 +1,25 @@
package com.example.petstoremobile.dtos;
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; }
}

View File

@@ -0,0 +1,17 @@
package com.example.petstoremobile.dtos;
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;
}
}

View File

@@ -0,0 +1,50 @@
package com.example.petstoremobile.dtos;
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;
}
}

View File

@@ -1,407 +1,551 @@
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;
@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;
@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);
}
}
);
}
@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 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);
}
});
}
private void setupRecyclerViews() {
// Set up Drawer menu to select conversation
chatAdapter = new ChatAdapter(chatList, this);
rvChatList.setLayoutManager(new LinearLayoutManager(getContext()));
rvChatList.setAdapter(chatAdapter);
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);
}
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();
}
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();
}
});
});
}
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();
}
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);
}
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
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;
}
}
}
}
//Helper function to load token and user id then connect to websocket
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
@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
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();
}
});
}
});
});
}
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
@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());
private void selectAttachment() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
attachmentLauncher.launch(intent);
}
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);
}
}
private void removeAttachment() {
pendingAttachmentUri = null;
binding.layoutAttachmentPreview.setVisibility(View.GONE);
}
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;
}
updateConversationPreview(dto.getConversationId(), dto.getContent());
if (currentUserId != null && currentUserId.equals(dto.getSenderId())) return;
String text = binding.etMessage.getText().toString().trim();
//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();
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();
}
});
}
@Override
public void onMessageReceived(MessageDTO dto) {
requireActivity().runOnUiThread(() -> {
if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) {
viewModel.addMessageLocally(dto);
}
viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), ""));
});
}
// When a new conversation is added, updates the chat preview
@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()));
}
});
}
@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);
});
}
@Override
public void onSocketClosed() {
if (!isAdded()) {
return;
}
loadConversations();
if (!isAdded()) return;
requireActivity().runOnUiThread(viewModel::loadConversations);
}
@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
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;
}
}
}
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
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
ChatNotificationService.activeConversationIdInUi = null;
if (stompChatManager != null) stompChatManager.disconnect();
}
}

View File

@@ -2,77 +2,72 @@ 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;
import dagger.hilt.android.AndroidEntryPoint;
//The Fragment for the displaying the list of entities to be viewed
@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 +78,58 @@ 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
@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);
}
}

View File

@@ -1,165 +1,109 @@
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 +114,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 +126,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 +144,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 +155,137 @@ 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);
private void setLoading(boolean loading) {
if (binding != null && binding.progressBar != null) {
binding.progressBar.setVisibility(loading ? View.VISIBLE : View.GONE);
}
}
@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();
}
});
}
}

View File

@@ -0,0 +1,172 @@
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;
@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;
@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();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
viewModel.loadInitialData();
}
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);
}
}
});
}
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);
});
}
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();
}
private void onStoreSelected() {
int pos = binding.spinnerStoreFilter.getSelectedItemPosition();
Long storeId = (pos > 0 && !storeList.isEmpty()) ? storeList.get(pos - 1).getId() : null;
viewModel.setStoreFilter(storeId);
}
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));
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -1,163 +1,277 @@
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;
@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;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(AdoptionListViewModel.class);
}
@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);
}
}
}
private void observeViewModel() {
viewModel.getAdoptions().observe(getViewLifecycleOwner(), list -> {
adoptionList.clear();
adoptionList.addAll(list);
updateCalendarDecorators();
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);
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 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());
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
adapter,
"adoption",
viewModel::bulkDeleteAdoptions,
() -> loadAdoptions(true)
);
}
@Override
public void onAdoptionClick(int position) {
openAdoptionDetails(position);
public void onResume() {
super.onResume();
loadAdoptions(true);
if (!isStaff()) viewModel.loadStores();
}
private void loadAdoptionData() {
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);
private void toggleCalendarMode() {
isMonthMode = !isMonthMode;
binding.calendarViewAdoption.state().edit()
.setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS)
.commit();
}
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);
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);
}
}
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
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);
});
}
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));
}
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);
}
}
});
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchAdoption, () -> loadAdoptions(true));
}
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Completed", "Pending", "Missed", "Cancelled"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, () -> loadAdoptions(true));
}
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, () -> loadAdoptions(true));
}
private void setupSwipeRefresh() {
binding.swipeRefreshAdoption.setOnRefreshListener(() -> loadAdoptions(true));
}
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);
}
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);
}
@Override
public void onAdoptionClick(int position) { openDetail(position); }
@Override
public void onSelectionChanged(int selectedCount) {
if (bulkDeleteHandler != null) {
bulkDeleteHandler.onSelectionChanged(selectedCount);
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -0,0 +1,379 @@
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.*;
@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 };
@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;
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");
});
}
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));
}
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);
}
// Filter Panel
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());
}
private void toggleFilters() {
filtersExpanded = !filtersExpanded;
binding.llFilterContent.setVisibility(filtersExpanded ? View.VISIBLE : View.GONE);
binding.tvFilterToggleIcon.setText(filtersExpanded ? "" : "");
}
private void applyPreset(int startOffset, int endOffset) {
binding.etFilterStartDate.setText(getDateString(startOffset));
binding.etFilterEndDate.setText(getDateString(endOffset));
updateFilterSummary();
applyFiltersFromUI();
}
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);
}
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();
}
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));
}
}
private String shortDate(String date) {
return (date != null && date.length() >= 10) ? date.substring(5) : date;
}
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));
}
// ViewModel Observation
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());
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
// Display
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");
}
}
// Chart Helpers
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);
}
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);
}
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();
}
}

View File

@@ -1,144 +1,229 @@
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;
@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());
@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);
}
@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);
}
}
}
private void observeViewModel() {
viewModel.getAppointments().observe(getViewLifecycleOwner(), list -> {
appointmentList.clear();
appointmentList.addAll(list);
updateCalendarDecorators();
adapter.notifyDataSetChanged();
});
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);
});
}
// 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);
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
adapter,
"appointment",
viewModel::bulkDeleteAppointments,
() -> loadAppointmentData(true)
);
}
@Override
public void onResume() {
super.onResume();
loadAppointmentData(true);
if (!isStaff()) viewModel.loadStores();
}
private void toggleCalendarMode() {
isMonthMode = !isMonthMode;
binding.calendarView.state().edit()
.setCalendarDisplayMode(isMonthMode ? CalendarMode.MONTHS : CalendarMode.WEEKS)
.commit();
}
private void setupMyAppointmentFilter() {
binding.btnMyAppointments.setOnClickListener(v -> {
loadAppointmentData(true);
});
}
private void loadCurrentUserInfo() {
authViewModel.getMe().observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
currentUserId = resource.data.getId();
}
});
}
private void setupFilterToggle() {
if (isStaff()) {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment, binding.spinnerStatus);
binding.spinnerStore.setVisibility(View.GONE);
} else {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchAppointment,
binding.spinnerStatus, binding.spinnerStore);
}
}
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);
});
}
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));
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchAppointment, () -> loadAppointmentData(true));
}
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, () -> loadAppointmentData(true));
}
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadAppointmentData(true));
}
private void setupSwipeRefresh() {
binding.swipeRefreshAppointment.setOnRefreshListener(() -> loadAppointmentData(true));
}
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());
AppointmentDTO a = appointmentList.get(position);
args.putLong("appointmentId", a.getAppointmentId());
}
detailFragment.setArguments(args);
detailFragment.setAppointmentFragment(this);
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) listFragment.loadFragment(detailFragment);
}
public void onAppointmentSaved(int position, Appointment appointment) {
if (position == -1) {
appointmentList.add(appointment);
} else {
appointmentList.set(position, appointment);
}
filterAppointments(etSearch.getText().toString());
}
public void onAppointmentDeleted(int position) {
appointmentList.remove(position);
filterAppointments(etSearch.getText().toString());
NavHostFragment.findNavController(this).navigate(R.id.nav_appointment_detail, args);
}
@Override
@@ -146,22 +231,74 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
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);
@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);
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
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);
}
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);
}
}
});
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -0,0 +1,162 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.CouponAdapter;
import com.example.petstoremobile.databinding.FragmentCouponBinding;
import com.example.petstoremobile.dtos.CouponDTO;
import com.example.petstoremobile.utils.Resource;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.CouponListViewModel;
import java.util.ArrayList;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class CouponFragment extends Fragment implements CouponAdapter.OnCouponClickListener {
private FragmentCouponBinding binding;
private CouponListViewModel viewModel;
private CouponAdapter adapter;
private final List<CouponDTO> couponList = new ArrayList<>();
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentCouponBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(CouponListViewModel.class);
setupRecyclerView();
setupSearch();
setupStatusFilter();
setupTypeFilter();
setupSwipeRefresh();
observeViewModel();
applyFilters(true);
binding.fabAddCoupon.setOnClickListener(v -> openDetail(-1));
binding.btnBulkDeleteCoupons.setOnClickListener(v -> confirmBulkDelete());
UIUtils.setupHamburgerMenu(binding.btnHamburgerCoupon, this);
UIUtils.setupFilterToggle(binding.btnToggleFilterCoupon, binding.layoutFilterCoupon, binding.etSearchCoupon,
binding.spinnerTypeCoupon, binding.spinnerStatusCoupon);
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getCoupons().observe(getViewLifecycleOwner(), list -> {
couponList.clear();
couponList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshCoupon.setRefreshing(loading);
});
}
private void setupRecyclerView() {
adapter = new CouponAdapter(couponList, this);
binding.recyclerViewCoupon.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewCoupon.setAdapter(adapter);
binding.recyclerViewCoupon.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy <= 0) return;
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewCoupon.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) {
applyFilters(false);
}
}
});
}
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Active", "Inactive"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCoupon, statuses, () -> applyFilters(true));
}
private void setupTypeFilter() {
String[] types = {"All Types", "FIXED", "PERCENT"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerTypeCoupon, types, () -> applyFilters(true));
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchCoupon, () -> applyFilters(true));
}
private void setupSwipeRefresh() {
binding.swipeRefreshCoupon.setOnRefreshListener(() -> applyFilters(true));
}
private void applyFilters(boolean reset) {
String statusStr = binding.spinnerStatusCoupon.getSelectedItem() != null ?
binding.spinnerStatusCoupon.getSelectedItem().toString() : "All Statuses";
Boolean active = null;
if (statusStr.equals("Active")) active = true;
else if (statusStr.equals("Inactive")) active = false;
String typeStr = binding.spinnerTypeCoupon.getSelectedItem() != null ?
binding.spinnerTypeCoupon.getSelectedItem().toString() : "All Types";
String discountType = typeStr.equals("All Types") ? null : typeStr;
viewModel.loadCoupons(reset, active, discountType, null);
}
private void openDetail(long id) {
Bundle args = new Bundle();
args.putLong("couponId", id);
Navigation.findNavController(requireView()).navigate(R.id.couponDetailFragment, args);
}
@Override
public void onCouponClick(CouponDTO coupon) {
openDetail(coupon.getCouponId());
}
@Override
public void onSelectionChanged(int count) {
binding.btnBulkDeleteCoupons.setVisibility(count > 0 ? View.VISIBLE : View.GONE);
}
private void confirmBulkDelete() {
new AlertDialog.Builder(requireContext())
.setTitle("Confirm Bulk Delete")
.setMessage("Are you sure you want to delete the selected coupons?")
.setPositiveButton("Delete", (dialog, which) -> {
List<Long> ids = new ArrayList<>(adapter.getSelectedIds());
viewModel.bulkDeleteCoupons(ids).observe(getViewLifecycleOwner(), resource -> {
if (resource.status == Resource.Status.SUCCESS) {
adapter.setSelectionMode(false);
applyFilters(true);
} else if (resource.status == Resource.Status.ERROR) {
UIUtils.showToast(requireContext(), resource.message);
}
});
})
.setNegativeButton("Cancel", null)
.show();
}
}

View File

@@ -0,0 +1,142 @@
package com.example.petstoremobile.fragments.listfragments;
import android.os.Bundle;
import android.view.*;
import androidx.annotation.NonNull;
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.CustomerAdapter;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentCustomerBinding;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.CustomerListViewModel;
import dagger.hilt.android.AndroidEntryPoint;
import java.util.*;
import javax.inject.Inject;
import javax.inject.Named;
@AndroidEntryPoint
public class CustomerFragment extends Fragment implements CustomerAdapter.OnCustomerClickListener {
private FragmentCustomerBinding binding;
private CustomerListViewModel viewModel;
private List<CustomerDTO> customerList = new ArrayList<>();
private CustomerAdapter adapter;
@Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentCustomerBinding.inflate(inflater, container, false);
viewModel = new ViewModelProvider(this).get(CustomerListViewModel.class);
setupRecyclerView();
setupSearch();
setupStatusFilter();
setupSwipeRefresh();
observeViewModel();
viewModel.loadCustomers(true);
binding.fabAddCustomer.setOnClickListener(v -> openDetail(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburgerCustomer, this);
UIUtils.setupFilterToggle(binding.btnToggleFilterCustomer, binding.layoutFilterCustomer,
binding.etSearchCustomer, binding.spinnerStatusCustomer);
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getFilteredCustomers().observe(getViewLifecycleOwner(), list -> {
customerList.clear();
customerList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshCustomer.setRefreshing(loading);
});
}
private void setupRecyclerView() {
adapter = new CustomerAdapter(customerList, this);
adapter.setBaseUrl(baseUrl);
adapter.setToken(tokenManager.getToken());
binding.recyclerViewCustomer.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewCustomer.setAdapter(adapter);
binding.recyclerViewCustomer.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy <= 0) return;
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewCustomer.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.loadCustomers(false);
}
}
});
}
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Active", "Inactive"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCustomer, statuses, this::applyFilters);
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchCustomer, this::applyFilters);
}
private void applyFilters() {
String query = binding.etSearchCustomer.getText().toString().trim();
String status = binding.spinnerStatusCustomer.getSelectedItem() != null ?
binding.spinnerStatusCustomer.getSelectedItem().toString() : "All Statuses";
viewModel.filter(query, status);
}
private void setupSwipeRefresh() {
binding.swipeRefreshCustomer.setOnRefreshListener(() -> viewModel.loadCustomers(true));
}
private void openDetail(int position) {
Bundle args = new Bundle();
if (position != -1) {
CustomerDTO c = customerList.get(position);
args.putLong("customerId", c.getCustomerId() != null ? c.getCustomerId() : -1);
args.putString("username", c.getUsername() != null ? c.getUsername() : "");
args.putString("firstName", c.getFirstName() != null ? c.getFirstName() : "");
args.putString("lastName", c.getLastName() != null ? c.getLastName() : "");
args.putString("email", c.getEmail() != null ? c.getEmail() : "");
args.putString("phone", c.getPhone() != null ? c.getPhone() : "");
args.putBoolean("active", Boolean.TRUE.equals(c.getActive()));
args.putInt("loyaltyPoints", c.getLoyaltyPoints() != null ? c.getLoyaltyPoints() : 0);
args.putBoolean("isEditing", true);
}
NavHostFragment.findNavController(this).navigate(R.id.nav_customer_detail, args);
}
@Override
public void onCustomerClick(int position) {
openDetail(position);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -1,162 +1,202 @@
package com.example.petstoremobile.fragments.listfragments;
// Added search/filter bar to filter inventory by item name or category.
// Added pull-to-refresh using SwipeRefreshLayout.
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.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.InventoryAdapter;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.InventoryDetailFragment;
import com.example.petstoremobile.models.Inventory;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentInventoryBinding;
import com.example.petstoremobile.dtos.InventoryDTO;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.InventoryListViewModel;
import com.example.petstoremobile.utils.SpinnerUtils;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener {
private List<Inventory> inventoryList = new ArrayList<>();
private List<Inventory> filteredList = new ArrayList<>();
private FragmentInventoryBinding binding;
private final List<InventoryDTO> inventoryList = new ArrayList<>();
private InventoryAdapter adapter;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
private ImageButton hamburger;
private InventoryListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
@Inject TokenManager tokenManager;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(InventoryListViewModel.class);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_inventory, container, false);
binding = FragmentInventoryBinding.inflate(inflater, container, false);
hamburger = view.findViewById(R.id.btnHamburger);
setupRecyclerView();
setupSearch();
setupStoreFilter();
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
loadInventoryData(); // TODO: Replace with actual API call when backend is ready
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
loadInventory(true);
FloatingActionButton fabAddInventory = view.findViewById(R.id.fabAddInventory);
fabAddInventory.setOnClickListener(v -> openInventoryDetails(-1));
binding.fabAddInventory.setOnClickListener(v -> openDetail(null));
//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();
}
});
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
return view;
return binding.getRoot();
}
// Filters inventory list by item name or category
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchInventory);
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) {
filterInventory(s.toString());
}
@Override public void afterTextChanged(Editable s) {}
});
}
private void filterInventory(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(inventoryList);
} else {
String lower = query.toLowerCase();
for (Inventory i : inventoryList) {
if (i.getItemName().toLowerCase().contains(lower)
|| i.getCategory().toLowerCase().contains(lower)
|| i.getSupplier().toLowerCase().contains(lower)) {
filteredList.add(i);
}
}
}
private void observeViewModel() {
viewModel.getInventory().observe(getViewLifecycleOwner(), list -> {
inventoryList.clear();
inventoryList.addAll(list);
adapter.notifyDataSetChanged();
}
});
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory);
swipeRefreshLayout.setOnRefreshListener(() -> {
loadInventoryData(); // TODO: Replace with actual API call
filterInventory(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.swipeRefreshInventory.setRefreshing(loading);
});
}
private void openInventoryDetails(int position) {
InventoryDetailFragment detailFragment = new InventoryDetailFragment();
Bundle args = new Bundle();
args.putInt("position", position);
if (position != -1) {
Inventory inventory = filteredList.get(position);
int realPosition = inventoryList.indexOf(inventory);
args.putInt("position", realPosition);
args.putInt("inventoryId", inventory.getInventoryId());
args.putString("itemName", inventory.getItemName());
args.putString("category", inventory.getCategory());
args.putInt("quantity", inventory.getQuantity());
args.putDouble("unitPrice", inventory.getUnitPrice());
args.putString("supplier", inventory.getSupplier());
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
adapter,
"inventory item",
viewModel::bulkDeleteInventory,
() -> loadInventory(true)
);
}
detailFragment.setArguments(args);
detailFragment.setInventoryFragment(this);
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) listFragment.loadFragment(detailFragment);
@Override
public void onResume() {
super.onResume();
if (!isStaff()) viewModel.loadStores();
}
public void onInventorySaved(int position, Inventory inventory) {
if (position == -1) {
inventoryList.add(inventory);
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
private void setupFilterToggle() {
if (isStaff()) {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory);
binding.spinnerStore.setVisibility(View.GONE);
} else {
inventoryList.set(position, inventory);
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchInventory, binding.spinnerStore);
}
filterInventory(etSearch.getText().toString());
}
public void onInventoryDeleted(int position) {
inventoryList.remove(position);
filterInventory(etSearch.getText().toString());
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchInventory, () -> loadInventory(true));
}
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadInventory(true));
}
private void setupRecyclerView() {
adapter = new InventoryAdapter(inventoryList, this);
binding.recyclerViewInventory.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewInventory.setAdapter(adapter);
binding.recyclerViewInventory.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy <= 0) return;
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewInventory.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) {
loadInventory(false);
}
}
});
}
private void setupSwipeRefresh() {
binding.swipeRefreshInventory.setOnRefreshListener(() -> loadInventory(true));
}
private void loadInventory(boolean reset) {
String query = binding.etSearchInventory != null ? binding.etSearchInventory.getText().toString().trim() : "";
if (query.isEmpty()) query = null;
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();
}
}
viewModel.loadInventory(reset, query, storeId);
}
private void openDetail(InventoryDTO inv) {
Bundle args = new Bundle();
if (inv != null) {
args.putLong("inventoryId", inv.getInventoryId());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_inventory_detail, args);
}
@Override
public void onInventoryClick(int position) {
openInventoryDetails(position);
if (position >= 0 && position < inventoryList.size()) {
openDetail(inventoryList.get(position));
}
}
private void loadInventoryData() {
inventoryList.clear();
inventoryList.add(new Inventory(1, "Dog Food - Large", "Food", 50, 25.99, "PetSupplies Co."));
inventoryList.add(new Inventory(2, "Cat Litter", "Hygiene", 30, 12.99, "CleanPaws Ltd."));
inventoryList.add(new Inventory(3, "Dog Leash", "Accessories", 4, 15.99, "PetGear Inc."));
inventoryList.add(new Inventory(4, "Bird Cage - Medium", "Housing", 8, 79.99, "BirdWorld"));
inventoryList.add(new Inventory(5, "Flea Treatment", "Medicine", 2, 34.99, "VetCare Supply"));
filteredList.clear();
filteredList.addAll(inventoryList);
}
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewInventory);
adapter = new InventoryAdapter(filteredList, this);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
@Override
public void onSelectionChanged(int selectedCount) {
if (bulkDeleteHandler != null) {
bulkDeleteHandler.onSelectionChanged(selectedCount);
}
}
}

View File

@@ -2,201 +2,225 @@ package com.example.petstoremobile.fragments.listfragments;
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 android.widget.Toast;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.PetAdapter;
import com.example.petstoremobile.api.PetApi;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.databinding.FragmentPetBinding;
import com.example.petstoremobile.dtos.PetDTO;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment;
import com.example.petstoremobile.fragments.listfragments.listprofilefragments.PetProfileFragment;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.example.petstoremobile.dtos.StoreDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.PetListViewModel;
import java.util.ArrayList;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener {
private FragmentPetBinding binding;
private List<PetDTO> petList = new ArrayList<>();
private List<PetDTO> filteredList = new ArrayList<>();
private ImageButton hamburger;
private PetAdapter adapter;
private PetApi api;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
private PetListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
@Inject @Named("baseUrl") String baseUrl;
@Inject TokenManager tokenManager;
//load pet view
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(PetListViewModel.class);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_pet, container, false);
binding = FragmentPetBinding.inflate(inflater, container, false);
//get retrofit
api = RetrofitClient.getPetApi(requireContext());
setupRecyclerView();
setupSearch();
setupStatusFilter();
setupSpeciesFilter();
setupStoreFilter();
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
hamburger = view.findViewById(R.id.btnHamburger);
binding.fabAddPet.setOnClickListener(v -> openPetDetails());
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
loadPetData();
UIUtils.setupHamburgerMenu(binding.btnHamburger, this);
//Add button to opens the add dialog
FloatingActionButton fabAddPet = view.findViewById(R.id.fabAddPet);
fabAddPet.setOnClickListener(v -> openPetDetails(-1));
//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();
}
});
return view;
return binding.getRoot();
}
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchPet);
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) {
filterPets(s.toString());
}
@Override public void afterTextChanged(Editable s) {}
});
}
private void filterPets(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(petList);
} else {
String lower = query.toLowerCase();
for (PetDTO p : petList) {
if (p.getPetName().toLowerCase().contains(lower)
|| p.getPetSpecies().toLowerCase().contains(lower)
|| p.getPetBreed().toLowerCase().contains(lower)) {
filteredList.add(p);
}
}
}
private void observeViewModel() {
viewModel.getPets().observe(getViewLifecycleOwner(), list -> {
petList.clear();
petList.addAll(list);
adapter.notifyDataSetChanged();
}
});
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshPet);
swipeRefreshLayout.setOnRefreshListener(() -> {
loadPetData();
viewModel.getStores().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStore, list,
StoreDTO::getStoreName, "All Stores", -1L, StoreDTO::getStoreId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPet.setRefreshing(loading);
});
viewModel.getSpeciesOptions().observe(getViewLifecycleOwner(), options -> {
String[] arr = options.toArray(new String[0]);
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, arr, () -> loadPetData(true));
});
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
adapter,
"pet",
viewModel::bulkDeletePets,
() -> loadPetData(true)
);
}
@Override
public void onResume() {
super.onResume();
loadPetData(true);
viewModel.loadSpecies();
if (!isStaff()) viewModel.loadStores();
}
private void setupFilterToggle() {
if (isStaff()) {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet,
binding.spinnerStatus, binding.spinnerSpecies);
binding.spinnerStore.setVisibility(View.GONE);
} else {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPet,
binding.spinnerStatus, binding.spinnerSpecies, binding.spinnerStore);
}
}
private boolean isStaff() {
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchPet, () -> loadPetData(true));
}
private void setupStatusFilter() {
String[] statuses = {"All Statuses", "Available", "Adopted", "Owned", "Pending"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, () -> loadPetData(true));
}
private void setupSpeciesFilter() {
String[] initial = {"All Species"};
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, () -> loadPetData(true));
}
private void setupStoreFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadPetData(true));
}
private void setupSwipeRefresh() {
binding.swipeRefreshPet.setOnRefreshListener(() -> loadPetData(true));
}
private void loadPetData(boolean reset) {
String query = binding.etSearchPet.getText().toString().trim();
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
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();
}
}
viewModel.loadPets(reset, query, status, species, storeId);
}
private void setupRecyclerView() {
adapter = new PetAdapter(petList, this);
adapter.setBaseUrl(baseUrl);
adapter.setToken(tokenManager.getToken());
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPets.setAdapter(adapter);
binding.recyclerViewPets.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy <= 0) return;
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewPets.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) {
loadPetData(false);
}
}
});
}
//Open pet profile
private void openPetProfile(int position) {
PetProfileFragment profileFragment = new PetProfileFragment();
//Make a bundle to pass data to the profile fragment
Bundle args = new Bundle();
PetDTO pet = filteredList.get(position);
args.putInt("petId", pet.getPetId().intValue());
args.putString("petName", pet.getPetName());
args.putString("petSpecies", pet.getPetSpecies());
args.putString("petBreed", pet.getPetBreed());
args.putInt("petAge", pet.getPetAge());
args.putString("petStatus", pet.getPetStatus());
try {
args.putDouble("petPrice", Double.parseDouble(pet.getPetPrice()));
} catch (Exception e) {
args.putDouble("petPrice", 0.0);
PetDTO pet = petList.get(position);
args.putLong("petId", pet.getPetId());
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_profile, args);
}
//send the bundle to the profile fragment to display
profileFragment.setArguments(args);
//get ListFragment to load the the pet profile view
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) {
listFragment.loadFragment(profileFragment);
}
private void openPetDetails() {
NavHostFragment.findNavController(this).navigate(R.id.nav_pet_detail);
}
//Open the pet detail view for adding
private void openPetDetails(int position) {
PetDetailFragment detailFragment = new PetDetailFragment();
//get ListFragment to load the detail view
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) {
listFragment.loadFragment(detailFragment);
}
}
// Called by PetAdapter when a row is clicked to open the details view
@Override
public void onPetClick(int position) {
openPetProfile(position);
}
// Helper function to get a list of all pets from the backend
private void loadPetData() {
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(true);
}
api.getAllPets(0, 100).enqueue(new Callback<PageResponse<PetDTO>>() {
@Override
public void onResponse(Call<PageResponse<PetDTO>> call, Response<PageResponse<PetDTO>> response) {
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
if (response.isSuccessful() && response.body() != null) {
petList.clear();
petList.addAll(response.body().getContent());
filterPets(etSearch.getText().toString());
} else {
Log.e("onResponse: ", response.message());
public void onSelectionChanged(int selectedCount) {
if (bulkDeleteHandler != null) {
bulkDeleteHandler.onSelectionChanged(selectedCount);
}
}
@Override
public void onFailure(Call<PageResponse<PetDTO>> call, Throwable t) {
if (swipeRefreshLayout != null) {
swipeRefreshLayout.setRefreshing(false);
}
Toast.makeText(getContext(),
"Failed to load pets", Toast.LENGTH_SHORT).show();
Log.e("onFailure: ", t.getMessage());
}
});
}
//set up the recyclerview and adapter
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewPets);
adapter = new PetAdapter(filteredList, this);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -1,140 +1,155 @@
package com.example.petstoremobile.fragments.listfragments;
// Added search/filter bar to filter products by name or category.
// Added pull-to-refresh using SwipeRefreshLayout.
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.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.ProductAdapter;
import com.example.petstoremobile.fragments.ListFragment;
import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductDetailFragment;
import com.example.petstoremobile.models.Product;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.example.petstoremobile.databinding.FragmentProductBinding;
import com.example.petstoremobile.dtos.DropdownDTO;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductListViewModel;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener {
private List<Product> productList = new ArrayList<>();
private List<Product> filteredList = new ArrayList<>();
private FragmentProductBinding binding;
private List<ProductDTO> productList = new ArrayList<>();
private ProductAdapter adapter;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
private ImageButton hamburger;
private ProductListViewModel viewModel;
@Inject @Named("baseUrl") String baseUrl;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductListViewModel.class);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_product, container, false);
binding = FragmentProductBinding.inflate(inflater, container, false);
hamburger = view.findViewById(R.id.btnHamburger);
setupRecyclerView();
setupSearch();
setupCategoryFilter();
setupSwipeRefresh();
setupFilterToggle();
observeViewModel();
loadProductData(); // TODO: Replace with actual API call when backend is ready
setupRecyclerView(view);
setupSearch(view);
setupSwipeRefresh(view);
binding.fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
FloatingActionButton fabAddProduct = view.findViewById(R.id.fabAddProduct);
fabAddProduct.setOnClickListener(v -> openProductDetails(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburgerProduct, 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();
}
});
return view;
return binding.getRoot();
}
// Filters products by name, description, or category
private void setupSearch(View view) {
etSearch = view.findViewById(R.id.etSearchProduct);
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) {
filterProducts(s.toString());
}
@Override public void afterTextChanged(Editable s) {}
});
}
private void filterProducts(String query) {
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(productList);
} else {
String lower = query.toLowerCase();
for (Product p : productList) {
if (p.getProductName().toLowerCase().contains(lower)
|| p.getCategory().toLowerCase().contains(lower)
|| p.getProductDesc().toLowerCase().contains(lower)) {
filteredList.add(p);
}
}
}
private void observeViewModel() {
viewModel.getProducts().observe(getViewLifecycleOwner(), list -> {
productList.clear();
productList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getCategories().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCategory, list,
DropdownDTO::getLabel, "All Categories", -1L, DropdownDTO::getId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshProduct.setRefreshing(loading);
});
}
private void setupSwipeRefresh(View view) {
swipeRefreshLayout = view.findViewById(R.id.swipeRefreshProduct);
swipeRefreshLayout.setOnRefreshListener(() -> {
loadProductData(); // TODO: Replace with actual API call
filterProducts(etSearch.getText().toString());
swipeRefreshLayout.setRefreshing(false);
@Override
public void onResume() {
super.onResume();
loadProductData(true);
viewModel.loadCategories();
}
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter,
binding.etSearchProduct, binding.spinnerCategory);
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchProduct, () -> loadProductData(true));
}
private void setupCategoryFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, () -> loadProductData(true));
}
private void setupSwipeRefresh() {
binding.swipeRefreshProduct.setOnRefreshListener(() -> loadProductData(true));
}
private void loadProductData(boolean reset) {
String query = binding.etSearchProduct.getText().toString().trim();
if (query.isEmpty()) query = null;
Long categoryId = null;
List<DropdownDTO> categories = viewModel.getCategories().getValue();
if (binding.spinnerCategory.getSelectedItemPosition() > 0 && categories != null && !categories.isEmpty()) {
categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getId();
}
viewModel.loadProducts(reset, query, categoryId);
}
private void setupRecyclerView() {
adapter = new ProductAdapter(productList, this);
adapter.setBaseUrl(baseUrl);
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewProducts.setAdapter(adapter);
binding.recyclerViewProducts.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy <= 0) return;
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewProducts.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) {
loadProductData(false);
}
}
});
}
private void openProductDetails(int position) {
ProductDetailFragment detailFragment = new ProductDetailFragment();
Bundle args = new Bundle();
args.putInt("position", position);
if (position != -1) {
Product product = filteredList.get(position);
int realPosition = productList.indexOf(product);
args.putInt("position", realPosition);
args.putInt("productId", product.getProductId());
args.putString("productName", product.getProductName());
args.putString("productDesc", product.getProductDesc());
args.putString("category", product.getCategory());
args.putDouble("productPrice", product.getProductPrice());
args.putInt("stockQuantity", product.getStockQuantity());
ProductDTO product = productList.get(position);
args.putLong("prodId", product.getProdId());
}
detailFragment.setArguments(args);
detailFragment.setProductFragment(this);
ListFragment listFragment = (ListFragment) getParentFragment();
if (listFragment != null) listFragment.loadFragment(detailFragment);
}
public void onProductSaved(int position, Product product) {
if (position == -1) {
productList.add(product);
} else {
productList.set(position, product);
}
filterProducts(etSearch.getText().toString());
}
public void onProductDeleted(int position) {
productList.remove(position);
filterProducts(etSearch.getText().toString());
NavHostFragment.findNavController(this).navigate(R.id.nav_product_detail, args);
}
@Override
@@ -142,21 +157,9 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
openProductDetails(position);
}
private void loadProductData() {
productList.clear();
productList.add(new Product(1, "Premium Dog Food", "High protein dry food for adult dogs", "Food", 45.99, 25));
productList.add(new Product(2, "Cat Toy Bundle", "Set of 5 interactive toys", "Toys", 19.99, 40));
productList.add(new Product(3, "Pet Shampoo", "Gentle formula for all breeds", "Grooming", 12.99, 60));
productList.add(new Product(4, "Dog Bed - Large", "Memory foam orthopedic bed", "Bedding", 89.99, 10));
productList.add(new Product(5, "Aquarium Starter Kit", "20-gallon tank with filter and light", "Aquatic", 129.99, 5));
filteredList.clear();
filteredList.addAll(productList);
}
private void setupRecyclerView(View view) {
RecyclerView recyclerView = view.findViewById(R.id.recyclerViewProducts);
adapter = new ProductAdapter(filteredList, this);
recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
recyclerView.setAdapter(adapter);
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}

View File

@@ -0,0 +1,199 @@
package com.example.petstoremobile.fragments.listfragments;
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.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.petstoremobile.R;
import com.example.petstoremobile.adapters.ProductSupplierAdapter;
import com.example.petstoremobile.databinding.FragmentProductSupplierBinding;
import com.example.petstoremobile.dtos.ProductDTO;
import com.example.petstoremobile.dtos.ProductSupplierDTO;
import com.example.petstoremobile.dtos.SupplierDTO;
import com.example.petstoremobile.utils.BulkDeleteHandler;
import com.example.petstoremobile.utils.SpinnerUtils;
import com.example.petstoremobile.utils.UIUtils;
import com.example.petstoremobile.viewmodels.ProductSupplierListViewModel;
import java.util.ArrayList;
import java.util.List;
import dagger.hilt.android.AndroidEntryPoint;
@AndroidEntryPoint
public class ProductSupplierFragment extends Fragment
implements ProductSupplierAdapter.OnProductSupplierClickListener {
private FragmentProductSupplierBinding binding;
private List<ProductSupplierDTO> psList = new ArrayList<>();
private ProductSupplierAdapter adapter;
private ProductSupplierListViewModel viewModel;
private BulkDeleteHandler bulkDeleteHandler;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = new ViewModelProvider(this).get(ProductSupplierListViewModel.class);
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentProductSupplierBinding.inflate(inflater, container, false);
setupRecyclerView();
setupSearch();
setupProductFilter();
setupSupplierFilter();
setupSwipeRefresh();
setupFilterToggle();
setupBulkDelete();
observeViewModel();
binding.fabAddPS.setOnClickListener(v -> openDetail(-1));
UIUtils.setupHamburgerMenu(binding.btnHamburgerPS, this);
return binding.getRoot();
}
private void observeViewModel() {
viewModel.getProductSuppliers().observe(getViewLifecycleOwner(), list -> {
psList.clear();
psList.addAll(list);
adapter.notifyDataSetChanged();
});
viewModel.getProducts().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerProduct, list,
ProductDTO::getProdName, "All Products", -1L, ProductDTO::getProdId);
});
viewModel.getSuppliers().observe(getViewLifecycleOwner(), list -> {
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerSupplier, list,
SupplierDTO::getSupCompany, "All Suppliers", -1L, SupplierDTO::getSupId);
});
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
binding.swipeRefreshPS.setRefreshing(loading);
});
}
private void setupBulkDelete() {
bulkDeleteHandler = new BulkDeleteHandler(
this,
binding.layoutBulkDelete,
binding.tvSelectionCount,
binding.btnBulkDelete,
adapter,
"relationship",
viewModel::bulkDeleteProductSuppliers,
() -> loadData(true)
);
}
@Override
public void onResume() {
super.onResume();
loadData(true);
viewModel.loadFilterData();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
private void setupFilterToggle() {
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchPS,
binding.spinnerProduct, binding.spinnerSupplier);
}
private void setupRecyclerView() {
adapter = new ProductSupplierAdapter(psList, this);
binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext()));
binding.recyclerViewPS.setAdapter(adapter);
binding.recyclerViewPS.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy <= 0) return;
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewPS.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) {
loadData(false);
}
}
});
}
private void setupSearch() {
UIUtils.attachSearch(binding.etSearchPS, () -> loadData(true));
}
private void setupProductFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, () -> loadData(true));
}
private void setupSupplierFilter() {
SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, () -> loadData(true));
}
private void setupSwipeRefresh() {
binding.swipeRefreshPS.setOnRefreshListener(() -> loadData(true));
}
private void loadData(boolean reset) {
String query = binding.etSearchPS.getText().toString().trim();
if (query.isEmpty()) query = null;
Long productId = null;
List<ProductDTO> products = viewModel.getProducts().getValue();
if (binding.spinnerProduct.getSelectedItemPosition() > 0 && products != null && !products.isEmpty()) {
productId = products.get(binding.spinnerProduct.getSelectedItemPosition() - 1).getProdId();
}
Long supplierId = null;
List<SupplierDTO> suppliers = viewModel.getSuppliers().getValue();
if (binding.spinnerSupplier.getSelectedItemPosition() > 0 && suppliers != null && !suppliers.isEmpty()) {
supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId();
}
viewModel.loadProductSuppliers(reset, query, productId, supplierId);
}
private void openDetail(int position) {
Bundle args = new Bundle();
if (position != -1) {
ProductSupplierDTO ps = psList.get(position);
args.putLong("productId", ps.getProductId());
args.putLong("supplierId", ps.getSupplierId());
}
NavHostFragment.findNavController(this).navigate(R.id.nav_product_supplier_detail, args);
}
@Override
public void onProductSupplierClick(int position) { openDetail(position); }
@Override
public void onSelectionChanged(int count) {
if (bulkDeleteHandler != null) {
bulkDeleteHandler.onSelectionChanged(count);
}
}
}

Some files were not shown because too many files have changed in this diff Show More