+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
- {{ getTitle(review) }}
-
-
{{ getAuthor(review) }}
-
-
-
-
- {{ review.user ? review.user.username : 'Unknown' }}
-
-
-
-
-
-
-
-
-
- {{ $formatDate(review.createdAt, dateFormat) }}
-
-
-
-
-
- edit
-
-
+
+
+
+
+ {{ getTitle(review) }}
+
+ by {{ getAuthor(review) }}
-
-
-
-
- "{{ review.reviewText }}"
-
-
-
+
+
+ "{{ review.reviewText }}"
+
+
+
+
+
+
+
+
+
+
+ {{ review.user ? review.user.username : 'Unknown' }}
+
+
+
+
+ {{ $formatDate(review.createdAt, dateFormat) }}
+
+
+
+
+
+
-
-
-
- chevron_left
-
- Page {{ page + 1 }} of {{ Math.ceil(totalReviews / limit) }}
-
- chevron_right
-
-
+
+
+
+ chevron_left
+
+ {{ page + 1 }} / {{ Math.ceil(totalReviews / limit) }}
+
+ chevron_right
+
@@ -121,15 +135,19 @@ export default {
data() {
return {
reviews: [],
+ reviewers: [],
+ ratingCounts: {},
totalReviews: 0,
loading: true,
selectedSort: 'newest',
+ sortDesc: true,
selectedUserFilter: null,
selectedRatingFilter: null,
searchQuery: '',
page: 0,
limit: 50,
- users: []
+ showUserMenu: false,
+ showRatingMenu: false
}
},
computed: {
@@ -144,25 +162,20 @@ export default {
},
sortItems() {
return [
- { value: 'newest', label: this.$strings.LabelSortNewestFirst },
- { value: 'oldest', label: this.$strings.LabelSortOldestFirst },
- { value: 'highest', label: this.$strings.LabelSortHighestRated },
- { value: 'lowest', label: this.$strings.LabelSortLowestRated }
+ { value: 'newest', text: this.$strings.LabelSortNewestFirst },
+ { value: 'oldest', text: this.$strings.LabelSortOldestFirst },
+ { value: 'highest', text: this.$strings.LabelSortHighestRated },
+ { value: 'lowest', text: this.$strings.LabelSortLowestRated }
]
},
- userFilterItems() {
- const items = [{ value: null, label: this.$strings.LabelAllUsers }]
- this.users.forEach((u) => {
- items.push({ value: u.id, label: u.username })
- })
- return items
+ selectedUserText() {
+ if (!this.selectedUserFilter) return this.$strings.LabelFilterByUser
+ const u = this.reviewers.find((r) => r.id === this.selectedUserFilter)
+ return u ? u.username : this.$strings.LabelFilterByUser
},
- ratingFilterItems() {
- const items = [{ value: null, label: this.$strings.LabelAllReviews }]
- for (let i = 5; i >= 1; i--) {
- items.push({ value: i, label: `${i} ${i === 1 ? 'Star' : 'Stars'}` })
- }
- return items
+ selectedRatingText() {
+ if (!this.selectedRatingFilter) return this.$strings.LabelFilterByRating
+ return `${this.selectedRatingFilter} ★`
},
filteredReviews() {
if (!this.searchQuery) return this.reviews
@@ -175,14 +188,11 @@ export default {
}
},
methods: {
- async fetchUsers() {
- if (!this.currentUser.isAdminOrUp) return
- try {
- const data = await this.$axios.$get('/api/users')
- this.users = data.users || []
- } catch (error) {
- console.error('Failed to fetch users', error)
- }
+ closeUserMenu() {
+ this.showUserMenu = false
+ },
+ closeRatingMenu() {
+ this.showRatingMenu = false
},
async fetchReviews() {
this.loading = true
@@ -199,8 +209,14 @@ export default {
}
const data = await this.$axios.$get(`/api/libraries/${this.libraryId}/reviews`, { params })
- this.reviews = data.reviews
- this.totalReviews = data.total
+ this.reviews = data.reviews || []
+ this.totalReviews = data.total || 0
+ if (data.reviewers) {
+ this.reviewers = data.reviewers
+ }
+ if (data.ratingCounts) {
+ this.ratingCounts = data.ratingCounts
+ }
} catch (error) {
console.error('Failed to fetch library reviews', error)
this.$toast.error('Failed to fetch reviews')
@@ -208,20 +224,22 @@ export default {
this.loading = false
}
},
- onSortInput(val) {
+ onSortChange(val) {
this.selectedSort = val
this.page = 0
this.fetchReviews()
},
- onUserFilterInput(val) {
+ setUserFilter(val) {
this.selectedUserFilter = val
this.selectedRatingFilter = null
+ this.showUserMenu = false
this.page = 0
this.fetchReviews()
},
- onRatingFilterInput(val) {
+ setRatingFilter(val) {
this.selectedRatingFilter = val
this.selectedUserFilter = null
+ this.showRatingMenu = false
this.page = 0
this.fetchReviews()
},
@@ -249,8 +267,13 @@ export default {
}
},
mounted() {
- this.fetchUsers()
this.fetchReviews()
}
}
+
+
diff --git a/server/controllers/ReviewController.js b/server/controllers/ReviewController.js
index 0cb0d0bcf..3d61fced7 100644
--- a/server/controllers/ReviewController.js
+++ b/server/controllers/ReviewController.js
@@ -272,11 +272,54 @@ class ReviewController {
return json
})
+ // Collect unique reviewers for the filter dropdown
+ const allReviewers = await Database.reviewModel.findAll({
+ attributes: ['userId'],
+ include: [
+ {
+ model: Database.libraryItemModel,
+ where: { libraryId },
+ required: true,
+ attributes: []
+ },
+ {
+ model: Database.userModel,
+ attributes: ['id', 'username']
+ }
+ ],
+ group: ['review.userId']
+ })
+ const reviewers = allReviewers
+ .filter((r) => r.user)
+ .map((r) => ({ id: r.user.id, username: r.user.username }))
+ .filter((v, i, a) => a.findIndex((t) => t.id === v.id) === i)
+
+ // Get counts for each rating level
+ const ratingCountsResults = await Database.reviewModel.findAll({
+ attributes: ['rating', [Database.sequelize.fn('COUNT', Database.sequelize.col('review.id')), 'count']],
+ include: [
+ {
+ model: Database.libraryItemModel,
+ where: { libraryId },
+ required: true,
+ attributes: []
+ }
+ ],
+ group: ['rating']
+ })
+ const ratingCounts = {}
+ for (let i = 1; i <= 5; i++) ratingCounts[i] = 0
+ ratingCountsResults.forEach((r) => {
+ ratingCounts[r.rating] = parseInt(r.get('count'))
+ })
+
res.json({
reviews: results,
total: count,
page: pageNum,
- limit: limitNum
+ limit: limitNum,
+ reviewers,
+ ratingCounts
})
} catch (error) {
Logger.error(`[ReviewController] Failed to fetch reviews for library ${libraryId}`, error)