Bläddra i källkod

feat(EditSong): Reports sidebar with functionality to sort by category or report

Signed-off-by: Jonathan <theflametrooper@gmail.com>
Jonathan 3 år sedan
förälder
incheckning
0e74a66d05

+ 141 - 23
backend/logic/actions/reports.js

@@ -11,22 +11,63 @@ const SongsModule = moduleManager.modules.songs;
 const CacheModule = moduleManager.modules.cache;
 const ActivitiesModule = moduleManager.modules.activities;
 
+CacheModule.runJob("SUB", {
+	channel: "report.issue.toggle",
+	cb: data => {
+		WSModule.runJob("EMIT_TO_ROOM", {
+			room: "admin.reports",
+			args: ["event:admin.report.issue.toggled", { data: { issueId: data.issueId, reportId: data.reportId } }]
+		});
+
+		WSModule.runJob("EMIT_TO_ROOM", {
+			room: `edit-song.${data.songId}`,
+			args: ["event:admin.report.issue.toggled", { data: { issueId: data.issueId, reportId: data.reportId } }]
+		});
+	}
+});
+
 CacheModule.runJob("SUB", {
 	channel: "report.resolve",
-	cb: reportId => {
+	cb: ({ reportId, songId }) => {
 		WSModule.runJob("EMIT_TO_ROOM", {
 			room: "admin.reports",
 			args: ["event:admin.report.resolved", { data: { reportId } }]
 		});
+
+		WSModule.runJob("EMIT_TO_ROOM", {
+			room: `edit-song.${songId}`,
+			args: ["event:admin.report.resolved", { data: { reportId } }]
+		});
 	}
 });
 
 CacheModule.runJob("SUB", {
 	channel: "report.create",
 	cb: report => {
-		WSModule.runJob("EMIT_TO_ROOM", {
-			room: "admin.reports",
-			args: ["event:admin.report.created", { data: { report } }]
+		console.log(report);
+
+		DBModule.runJob("GET_MODEL", { modelName: "user" }, this).then(userModel => {
+			userModel
+				.findById(report.createdBy)
+				.select({ avatar: -1, name: -1, username: -1 })
+				.exec((err, { avatar, name, username }) => {
+					report.createdBy = {
+						avatar,
+						name,
+						username,
+						_id: report.createdBy
+					};
+
+					WSModule.runJob("EMIT_TO_ROOM", {
+						room: "admin.reports",
+						args: ["event:admin.report.created", { data: { report } }]
+					});
+
+					WSModule.runJob("EMIT_TO_ROOM", {
+						room: `edit-song.${report.song._id}`,
+						args: ["event:admin.report.created", { data: { report } }]
+					});
+				});
 		});
 	}
 });
@@ -87,22 +128,38 @@ export default {
 	 */
 	getReportsForSong: isAdminRequired(async function getReportsForSong(session, songId, cb) {
 		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
+		const userModel = await DBModule.runJob("GET_MODEL", { modelName: "user" }, this);
 
 		async.waterfall(
 			[
-				next => {
-					reportModel
-						.find({ song: { _id: songId }, resolved: false })
-						.sort({ released: "desc" })
-						.exec(next);
-				},
+				next =>
+					reportModel.find({ "song._id": songId, resolved: false }).sort({ createdAt: "desc" }).exec(next),
 
 				(_reports, next) => {
 					const reports = [];
-					for (let i = 0; i < _reports.length; i += 1) {
-						reports.push(_reports[i]._id);
-					}
-					next(null, reports);
+
+					async.each(
+						_reports,
+						(report, cb) => {
+							userModel
+								.findById(report.createdBy)
+								.select({ avatar: -1, name: -1, username: -1 })
+								.exec((err, { avatar, name, username }) => {
+									reports.push({
+										...report._doc,
+										createdBy: {
+											avatar,
+											name,
+											username,
+											_id: report.createdBy
+										}
+									});
+
+									return cb(err);
+								});
+						},
+						err => next(err, reports)
+					);
 				}
 			],
 			async (err, reports) => {
@@ -119,7 +176,7 @@ export default {
 	}),
 
 	/**
-	 * Resolves a reported issue
+	 * Resolves a report as a whole
 	 *
 	 * @param {object} session - the session object automatically added by the websocket
 	 * @param {string} reportId - the id of the report that is getting resolved
@@ -131,19 +188,21 @@ export default {
 		async.waterfall(
 			[
 				next => {
-					reportModel.findOne({ _id: reportId }).exec(next);
+					reportModel.findById(reportId).exec(next);
 				},
 
 				(report, next) => {
 					if (!report) return next("Report not found.");
+
 					report.resolved = true;
+
 					return report.save(err => {
 						if (err) return next(err.message);
-						return next();
+						return next(null, report.song._id);
 					});
 				}
 			],
-			async err => {
+			async (err, songId) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 					this.log(
@@ -156,7 +215,7 @@ export default {
 
 				CacheModule.runJob("PUB", {
 					channel: "report.resolve",
-					value: reportId
+					value: { reportId, songId }
 				});
 
 				this.log("SUCCESS", "REPORTS_RESOLVE", `User "${session.userId}" resolved report "${reportId}".`);
@@ -169,6 +228,65 @@ export default {
 		);
 	}),
 
+	/**
+	 * Resolves/Unresolves an issue within a report
+	 *
+	 * @param {object} session - the session object automatically added by the websocket
+	 * @param {string} reportId - the id of the report that is getting resolved
+	 * @param {string} issueId - the id of the issue within the report
+	 * @param {Function} cb - gets called with the result
+	 */
+	toggleIssue: isAdminRequired(async function toggleIssue(session, reportId, issueId, cb) {
+		const reportModel = await DBModule.runJob("GET_MODEL", { modelName: "report" }, this);
+
+		async.waterfall(
+			[
+				next => {
+					reportModel.findById(reportId).exec(next);
+				},
+
+				(report, next) => {
+					if (!report) return next("Report not found.");
+
+					const issue = report.issues.find(issue => issue._id.toString() === issueId);
+					issue.resolved = !issue.resolved;
+
+					return report.save(err => {
+						if (err) return next(err.message);
+						return next(null, report.song._id);
+					});
+				}
+			],
+			async (err, songId) => {
+				if (err) {
+					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
+					this.log(
+						"ERROR",
+						"REPORTS_TOGGLE_ISSUE",
+						`Resolving an issue within report "${reportId}" failed by user "${session.userId}". "${err}"`
+					);
+					return cb({ status: "error", message: err });
+				}
+
+				CacheModule.runJob("PUB", {
+					channel: "report.issue.toggle",
+					value: { reportId, issueId, songId }
+				});
+
+				this.log(
+					"SUCCESS",
+					"REPORTS_TOGGLE_ISSUE",
+					`User "${session.userId}" resolved an issue in report "${reportId}".`
+				);
+
+				return cb({
+					status: "success",
+					message: "Successfully resolved issue within report"
+				});
+			}
+		);
+	}),
+
 	/**
 	 * Creates a new report
 	 *
@@ -215,10 +333,10 @@ export default {
 							createdAt: Date.now(),
 							...report
 						},
-						err => next(err, song)
+						(err, report) => next(err, report, song)
 					)
 			],
-			async (err, song) => {
+			async (err, report, song) => {
 				if (err) {
 					err = await UtilsModule.runJob("GET_ERROR", { error: err }, this);
 					this.log(
@@ -230,7 +348,7 @@ export default {
 				}
 
 				ActivitiesModule.runJob("ADD_ACTIVITY", {
-					userId: report.createdBy,
+					userId: session.userId,
 					type: "song__report",
 					payload: {
 						message: `Reported song <youtubeId>${song.title} by ${song.artists.join(", ")}</youtubeId>`,
@@ -241,7 +359,7 @@ export default {
 
 				CacheModule.runJob("PUB", {
 					channel: "report.create",
-					report
+					value: report
 				});
 
 				this.log("SUCCESS", "REPORTS_CREATE", `User "${session.userId}" created report for "${youtubeId}".`);

+ 1 - 1
backend/logic/db/index.js

@@ -11,7 +11,7 @@ const REQUIRED_DOCUMENT_VERSIONS = {
 	playlist: 4,
 	punishment: 1,
 	queueSong: 1,
-	report: 4,
+	report: 5,
 	song: 5,
 	station: 5,
 	user: 3

+ 4 - 2
backend/logic/db/schemas/report.js

@@ -11,10 +11,12 @@ export default {
 				enum: ["custom", "video", "title", "duration", "artists", "thumbnail"],
 				required: true
 			},
-			info: { type: String, required: true }
+			title: { type: String, required: true },
+			description: { type: String, required: false },
+			resolved: { type: Boolean, default: false, required: true }
 		}
 	],
 	createdBy: { type: String, required: true },
 	createdAt: { type: Date, default: Date.now, required: true },
-	documentVersion: { type: Number, default: 4, required: true }
+	documentVersion: { type: Number, default: 5, required: true }
 };

+ 60 - 0
backend/logic/migration/migrations/migration13.js

@@ -0,0 +1,60 @@
+import async from "async";
+
+/**
+ * Migration 13
+ *
+ * Migration for allowing titles, descriptions and individual resolving for report issues
+ *
+ * @param {object} MigrationModule - the MigrationModule
+ * @returns {Promise} - returns promise
+ */
+export default async function migrate(MigrationModule) {
+	const reportModel = await MigrationModule.runJob("GET_MODEL", { modelName: "report" }, this);
+
+	return new Promise((resolve, reject) => {
+		async.waterfall(
+			[
+				next => {
+					this.log("INFO", `Migration 13. Finding reports with document version 4.`);
+					reportModel.find({ documentVersion: 4 }, (err, reports) => {
+						if (err) next(err);
+						else {
+							async.eachLimit(
+								reports.map(reporti => reporti._doc),
+								1,
+								(reporti, next) => {
+									const { issues } = reporti;
+
+									issues.forEach(issue => {
+										issue.title = issue.info;
+										issue.resolved = reporti.resolved;
+										delete issue.info;
+									});
+
+									reportModel.updateOne(
+										{ _id: reporti._id },
+										{
+											$set: {
+												documentVersion: 5,
+												issues
+											}
+										},
+										next
+									);
+								},
+								err => {
+									this.log("INFO", `Migration 13. Reports found: ${reports.length}.`);
+									next(err);
+								}
+							);
+						}
+					});
+				}
+			],
+			err => {
+				if (err) reject(new Error(err));
+				else resolve();
+			}
+		);
+	});
+}

+ 370 - 413
frontend/src/components/modals/EditSong/Tabs/Reports.vue

@@ -1,346 +1,267 @@
 <template>
-	<div class="reports-tab">
-		<div class="report-summary">
-			<div class="report-summary-header">
-				<p class="report-summary-title">Summary</p>
-				<div class="report-summary-actions universal-item-actions">
-					<i
-						class="material-icons resolve-all-icon"
-						content="Resolve all"
-						v-tippy
-						>done_all</i
-					>
-				</div>
-			</div>
-			<div class="report-summary-items">
-				<div class="report-summary-item report-summary-item-resolved">
-					<i
-						class="material-icons duration-icon report-summary-item-left-icon"
-						content="Duration"
-						v-tippy
-						>timer</i
-					>
-					<p class="report-summary-item-description">
-						<span class="report-summary-item-description-1"
-							>Video starts too soon</span
-						>
-						<span class="report-summary-item-description-2"
-							>The video ends 3 seconds too late</span
-						>
-						<span class="report-summary-item-description-2"
-							>The video ends 3 seconds too late</span
-						>
-						<span class="report-summary-item-description-2"
-							>The video ends 3 seconds too late</span
-						>
-					</p>
-					<div
-						class="report-summary-item-actions universal-item-actions"
-					>
-						<i
-							class="material-icons resolve-icon"
-							content="Resolve"
-							v-tippy
-							>done</i
-						>
-					</div>
-				</div>
-				<div class="report-summary-item report-summary-item-resolved">
-					<i
-						class="material-icons thumbnail-icon report-summary-item-left-icon"
-						content="Thumbnail"
-						v-tippy
-						>image</i
-					>
-					<p class="report-summary-item-description">
-						<span class="report-summary-item-description-1"
-							>Video starts too soon</span
-						>
-					</p>
-					<div
-						class="report-summary-item-actions universal-item-actions"
-					>
-						<i
-							class="material-icons resolve-icon"
-							content="Resolve"
-							v-tippy
-							>done</i
-						>
-					</div>
-				</div>
-			</div>
+	<div class="reports-tab tabs-container">
+		<div class="tab-selection">
+			<button
+				class="button is-default"
+				ref="sort-by-report-tab"
+				:class="{ selected: tab === 'sort-by-report' }"
+				@click="showTab('sort-by-report')"
+			>
+				Sort by Report
+			</button>
+			<button
+				class="button is-default"
+				ref="sort-by-category-tab"
+				:class="{ selected: tab === 'sort-by-category' }"
+				@click="showTab('sort-by-category')"
+			>
+				Sort by Category
+			</button>
 		</div>
-		<hr />
-		<div class="report-items">
-			<div class="report-item">
-				<div class="report-item-header">
-					<p class="report-item-summary">Duration issues</p>
-					<div class="report-item-actions universal-item-actions">
-						<i
-							class="material-icons resolve-all-icon"
-							content="Resolve all"
-							v-tippy
-							>done_all</i
-						>
-					</div>
-				</div>
-				<div class="report-sub-items">
-					<div class="report-sub-item report-sub-item-resolved">
-						<i
-							class="material-icons duration-icon report-sub-item-left-icon"
-							content="Duration"
-							v-tippy
-							>timer</i
-						>
-						<p class="report-sub-item-description">
-							<span class="report-sub-item-description-1"
-								>Video starts too soon</span
-							>
-						</p>
-						<div
-							class="report-sub-item-actions universal-item-actions"
-						>
-							<i
-								class="material-icons unresolve-icon"
-								content="Unresolve"
-								v-tippy
-								>remove</i
-							>
+
+		<div class="tab" v-if="tab === 'sort-by-category'">
+			<div class="report-items" v-if="reports.length > 0">
+				<div
+					class="report-item"
+					v-for="(issues, category) in sortedByCategory"
+					:key="category"
+				>
+					<div class="report-item-header">
+						<div class="report-item-info">
+							<div class="report-item-icon">
+								<i
+									class="material-icons"
+									:content="category"
+									v-tippy="{ theme: 'info' }"
+								>
+									{{ icons[category] }}
+								</i>
+							</div>
+
+							<div class="report-item-summary">
+								<p class="report-item-summary-title">
+									{{ category }} Issues
+								</p>
+							</div>
 						</div>
-					</div>
-					<div class="report-sub-item report-sub-item-unresolved">
-						<i
-							class="material-icons duration-icon report-sub-item-left-icon"
-							content="Duration"
-							v-tippy
-							>timer</i
-						>
-						<p class="report-sub-item-description">
-							<span class="report-sub-item-description-1"
-								>Video ends too late</span
-							>
-							<span class="report-sub-item-description-2"
-								>The video ends 3 seconds too late</span
-							>
-						</p>
-						<div
-							class="report-sub-item-actions universal-item-actions"
-						>
+
+						<!-- <div class="report-item-actions universal-item-actions">
 							<i
 								class="material-icons resolve-icon"
-								content="Resolve"
+								content="Resolve all"
 								v-tippy
-								>done</i
+								@click="resolve(99)"
 							>
-						</div>
-					</div>
-				</div>
-			</div>
-			<div class="report-item">
-				<div class="report-item-header">
-					<p class="report-item-summary">
-						Duration and thumbnail issues
-					</p>
-					<div class="report-item-actions universal-item-actions">
-						<i
-							class="material-icons resolve-all-icon"
-							content="Resolve all"
-							v-tippy
-							>done_all</i
-						>
+								done_all
+							</i>
+						</div> -->
 					</div>
-				</div>
-				<div class="report-sub-items">
-					<div class="report-sub-item report-sub-item-resolved">
-						<i
-							class="material-icons duration-icon report-sub-item-left-icon"
-							content="Duration"
-							v-tippy
-							>timer</i
-						>
-						<p class="report-sub-item-description">
-							<span class="report-sub-item-description-1"
-								>Video starts too soon</span
-							>
-						</p>
+					<div class="report-sub-items">
 						<div
-							class="report-sub-item-actions universal-item-actions"
+							class="report-sub-item report-sub-item-unresolved"
+							:class="[
+								'report',
+								issue.resolved
+									? 'report-sub-item-resolved'
+									: 'report-sub-item-unresolved'
+							]"
+							v-for="(issue, issueIndex) in issues"
+							:key="issueIndex"
 						>
 							<i
-								class="material-icons unresolve-icon"
-								content="Unresolve"
+								class="material-icons duration-icon report-sub-item-left-icon"
+								:content="issue.category"
 								v-tippy
-								>remove</i
-							>
-						</div>
-					</div>
-					<div class="report-sub-item report-sub-item-unresolved">
-						<i
-							class="material-icons thumbnail-icon report-sub-item-left-icon"
-							content="Thumbnail"
-							v-tippy
-							>image</i
-						>
-						<p class="report-sub-item-description">
-							<span class="report-sub-item-description-1"
-								>Thumbnail is incorrect</span
 							>
-						</p>
-						<div
-							class="report-sub-item-actions universal-item-actions"
-						>
-							<i
-								class="material-icons resolve-icon"
-								content="Resolve"
-								v-tippy
-								>done</i
+								{{ icons[category] }}
+							</i>
+
+							<p class="report-sub-item-info">
+								<span class="report-sub-item-title">
+									{{ issue.title }}
+								</span>
+								<span
+									class="report-sub-item-description"
+									v-if="issue.description"
+								>
+									{{ issue.description }}
+								</span>
+							</p>
+
+							<div
+								class="report-sub-item-actions universal-item-actions"
 							>
+								<i
+									class="material-icons resolve-icon"
+									content="Resolve"
+									v-tippy
+									v-if="!issue.resolved"
+									@click="
+										toggleIssue(issue.reportId, issue._id)
+									"
+								>
+									done
+								</i>
+								<i
+									class="material-icons unresolve-icon"
+									content="Unresolve"
+									v-tippy
+									v-else
+									@click="
+										toggleIssue(issue.reportId, issue._id)
+									"
+								>
+									remove
+								</i>
+							</div>
 						</div>
 					</div>
 				</div>
 			</div>
-			<div class="report-item">
-				<div class="report-item-header">
-					<p class="report-item-summary">Various issues</p>
-					<div class="report-item-actions universal-item-actions">
-						<i
-							class="material-icons resolve-all-icon"
-							content="Resolve all"
-							v-tippy
-							>done_all</i
-						>
-					</div>
-				</div>
-				<div class="report-sub-items">
-					<div class="report-sub-item report-sub-item-unresolved">
-						<i
-							class="material-icons duration-icon report-sub-item-left-icon"
-							content="Duration"
-							v-tippy
-							>timer</i
-						>
-						<p class="report-sub-item-description">
-							<span class="report-sub-item-description-1"
-								>Video starts too soon</span
-							>
-						</p>
-						<div
-							class="report-sub-item-actions universal-item-actions"
-						>
-							<i
-								class="material-icons resolve-icon"
-								content="Resolve"
-								v-tippy
-								>done</i
-							>
+			<p class="no-reports" v-else>There are no reports for this song.</p>
+		</div>
+
+		<div class="tab" v-if="tab === 'sort-by-report'">
+			<div class="report-items" v-if="reports.length > 0">
+				<div
+					class="report-item"
+					v-for="report in reports"
+					:key="report._id"
+				>
+					<div class="report-item-header">
+						<div class="report-item-info">
+							<div class="report-item-icon">
+								<profile-picture
+									:avatar="report.createdBy.avatar"
+									:name="
+										report.createdBy.name
+											? report.createdBy.name
+											: report.createdBy.username
+									"
+								/>
+							</div>
+
+							<div class="report-item-summary">
+								<p class="report-item-summary-title">
+									Reported by
+									<router-link
+										:to="{
+											path: `/u/${report.createdBy.username}`
+										}"
+										:title="report.createdBy._id"
+										@click="closeModal('editSong')"
+									>
+										{{ report.createdBy.username }}
+									</router-link>
+								</p>
+								<p class="report-item-summary-description">
+									{{
+										formatDistance(
+											parseISO(report.createdAt),
+											new Date(),
+											{
+												addSuffix: true
+											}
+										)
+									}}
+								</p>
+							</div>
 						</div>
-					</div>
-					<div class="report-sub-item report-sub-item-unresolved">
-						<i
-							class="material-icons video-icon report-sub-item-left-icon"
-							content="Video"
-							v-tippy
-							>tv</i
-						>
-						<p class="report-sub-item-description">
-							<span class="report-sub-item-description-1"
-								>Video doesn't exist</span
-							>
-						</p>
-						<div
-							class="report-sub-item-actions universal-item-actions"
-						>
+
+						<div class="report-item-actions universal-item-actions">
 							<i
 								class="material-icons resolve-icon"
-								content="Resolve"
+								content="Resolve all"
 								v-tippy
-								>done</i
+								@click="resolve(report._id)"
 							>
+								done_all
+							</i>
 						</div>
 					</div>
-					<div class="report-sub-item report-sub-item-unresolved">
-						<i
-							class="material-icons thumbnail-icon report-sub-item-left-icon"
-							content="Thumbnail"
-							v-tippy
-							>image</i
-						>
-						<p class="report-sub-item-description">
-							<span class="report-sub-item-description-1"
-								>Thumbnail is incorrect</span
-							>
-						</p>
+					<div class="report-sub-items">
 						<div
-							class="report-sub-item-actions universal-item-actions"
+							class="report-sub-item report-sub-item-unresolved"
+							:class="[
+								'report',
+								issue.resolved
+									? 'report-sub-item-resolved'
+									: 'report-sub-item-unresolved'
+							]"
+							v-for="(issue, issueIndex) in report.issues"
+							:key="issueIndex"
 						>
 							<i
-								class="material-icons resolve-icon"
-								content="Resolve"
+								class="material-icons duration-icon report-sub-item-left-icon"
+								:content="issue.category"
 								v-tippy
-								>done</i
-							>
-						</div>
-					</div>
-					<div class="report-sub-item report-sub-item-unresolved">
-						<i
-							class="material-icons artists-icon report-sub-item-left-icon"
-							content="Artists"
-							v-tippy
-							>record_voice_over</i
-						>
-						<p class="report-sub-item-description">
-							<span class="report-sub-item-description-1"
-								>Artists is missing</span
 							>
-						</p>
-						<div
-							class="report-sub-item-actions universal-item-actions"
-						>
-							<i
-								class="material-icons resolve-icon"
-								content="Resolve"
-								v-tippy
-								>done</i
-							>
-						</div>
-					</div>
-					<div class="report-sub-item report-sub-item-unresolved">
-						<i
-							class="material-icons title-icon report-sub-item-left-icon"
-							content="Title"
-							v-tippy
-							>title</i
-						>
-						<p class="report-sub-item-description">
-							<span class="report-sub-item-description-1"
-								>Title is misspelled</span
-							>
-						</p>
-						<div
-							class="report-sub-item-actions universal-item-actions"
-						>
-							<i
-								class="material-icons resolve-icon"
-								content="Resolve"
-								v-tippy
-								>done</i
+								{{ icons[issue.category] }}
+							</i>
+							<p class="report-sub-item-info">
+								<span class="report-sub-item-title">
+									{{ issue.title }}
+								</span>
+								<span
+									class="report-sub-item-description"
+									v-if="issue.description"
+								>
+									{{ issue.description }}
+								</span>
+							</p>
+
+							<div
+								class="report-sub-item-actions universal-item-actions"
 							>
+								<i
+									class="material-icons resolve-icon"
+									content="Resolve"
+									v-tippy
+									v-if="!issue.resolved"
+									@click="toggleIssue(report._id, issue._id)"
+								>
+									done
+								</i>
+								<i
+									class="material-icons unresolve-icon"
+									content="Unresolve"
+									v-tippy
+									v-else
+									@click="toggleIssue(report._id, issue._id)"
+								>
+									remove
+								</i>
+							</div>
 						</div>
 					</div>
 				</div>
 			</div>
+			<p class="no-reports" v-else>There are no reports for this song.</p>
 		</div>
 	</div>
 </template>
 
 <script>
-import { mapState, mapGetters /* , mapActions */ } from "vuex";
+import ProfilePicture from "@/components/ProfilePicture.vue";
 
-// import Toast from "toasters";
+import { mapState, mapGetters, mapActions } from "vuex";
+import { formatDistance, parseISO } from "date-fns";
+import Toast from "toasters";
 
 export default {
+	components: { ProfilePicture },
 	data() {
-		return {};
+		return {
+			tab: "sort-by-report",
+			icons: {
+				duration: "timer",
+				video: "tv",
+				thumbnail: "image",
+				artists: "record_voice_over",
+				title: "title",
+				custom: "lightbulb"
+			}
+		};
 	},
 	computed: {
 		...mapState("modals/editSong", {
@@ -348,104 +269,112 @@ export default {
 		}),
 		...mapGetters({
 			socket: "websockets/getSocket"
-		})
+		}),
+		sortedByCategory() {
+			const categories = {};
+
+			this.reports.forEach(report =>
+				report.issues.forEach(issue => {
+					if (categories[issue.category])
+						categories[issue.category].push({
+							...issue,
+							reportId: report._id
+						});
+					else
+						categories[issue.category] = [
+							{ ...issue, reportId: report._id }
+						];
+				})
+			);
+
+			return categories;
+		}
 	},
-	mounted() {}
-	// methods: {
-	// 	...mapActions("modals/editSong", ["selectDiscogsInfo"])
-	// }
+	mounted() {
+		this.socket.on("event:admin.report.created", res =>
+			this.reports.unshift(res.data.report)
+		);
+
+		this.socket.on("event:admin.report.resolved", res =>
+			this.resolveReport(res.data.reportId)
+		);
+
+		this.socket.on("event:admin.report.issue.toggled", res =>
+			this.reports.forEach((report, index) => {
+				if (report._id === res.data.reportId) {
+					const issue = this.reports[index].issues.find(
+						issue => issue._id.toString() === res.data.issueId
+					);
+
+					issue.resolved = !issue.resolved;
+				}
+			})
+		);
+	},
+	methods: {
+		showTab(tab) {
+			this.$refs[`${tab}-tab`].scrollIntoView();
+			this.tab = tab;
+		},
+		resolve(reportId) {
+			this.socket.dispatch(
+				"reports.resolve",
+				reportId,
+				res => new Toast(res.message)
+			);
+		},
+		toggleIssue(reportId, issueId) {
+			this.socket.dispatch(
+				"reports.toggleIssue",
+				reportId,
+				issueId,
+				res => {
+					if (res.status !== "success") new Toast(res.message);
+				}
+			);
+		},
+		formatDistance,
+		parseISO,
+		...mapActions("modals/editSong", ["resolveReport"]),
+		...mapActions("modalVisibility", ["closeModal"])
+	}
 };
 </script>
 
 <style lang="scss" scoped>
-.report-summary {
-	background-color: var(--white);
-	border: 0.5px solid var(--primary-color);
-	border-radius: 5px;
-	margin-bottom: 16px;
-	padding: 8px;
-
-	.report-summary-header {
+.tabs-container {
+	.tab-selection {
 		display: flex;
-		margin-bottom: 8px;
-
-		.report-summary-title {
-			font-weight: 700;
-			flex: 1;
-		}
-
-		.report-summary-actions {
-			height: 24px;
-			margin-right: 4px;
-
-			.resolve-all-icon {
-				color: var(--green);
+		overflow-x: auto;
+		.button {
+			border-radius: 0;
+			border: 0;
+			text-transform: uppercase;
+			font-size: 14px;
+			color: var(--dark-grey-3);
+			background-color: var(--light-grey-2);
+			flex-grow: 1;
+			height: 32px;
+
+			&:not(:first-of-type) {
+				margin-left: 5px;
 			}
 		}
-	}
 
-	.report-summary-items {
-		.report-summary-item {
-			border: 0.5px solid var(--black);
-			margin-top: -1px;
-			line-height: 24px;
-			display: flex;
-			padding: 4px;
-			display: flex;
-
-			&:first-child {
-				border-radius: 3px 3px 0 0;
-			}
-
-			&:last-child {
-				border-radius: 0 0 3px 3px;
-			}
-
-			.report-summary-item-left-icon {
-				margin-right: 8px;
-				margin-top: auto;
-				margin-bottom: auto;
-			}
-
-			.report-summary-item-description {
-				flex: 1;
-				display: flex;
-				flex-direction: column;
-
-				.report-summary-item-description-1 {
-					font-size: 14px;
-				}
-
-				.report-summary-item-description-2::before {
-					content: "- ";
-					position: absolute;
-					left: 0px;
-				}
-
-				.report-summary-item-description-2 {
-					font-size: 12px;
-					line-height: 20px;
-					padding-left: 12px;
-					position: relative;
-				}
-			}
-
-			.report-summary-item-actions {
-				height: 24px;
-				margin-left: 8px;
-				margin-top: auto;
-				margin-bottom: auto;
-
-				i {
-					cursor: pointer;
-				}
-
-				.resolve-icon {
-					color: var(--green);
-				}
-			}
+		.selected {
+			background-color: var(--primary-color) !important;
+			color: var(--white) !important;
+			font-weight: 600;
 		}
 	}
+	.tab {
+		padding: 15px 0;
+		border-radius: 0;
+	}
+}
+
+.no-reports {
+	text-align: center;
 }
 
 .report-items {
@@ -458,20 +387,52 @@ export default {
 
 		.report-item-header {
 			display: flex;
+			align-items: center;
+			justify-content: space-between;
 			margin-bottom: 8px;
+			background-color: var(--light-grey);
+			padding: 5px;
+			border-radius: 5px;
+
+			.report-item-info {
+				display: flex;
+				align-items: center;
+
+				.report-item-icon {
+					display: flex;
+					align-items: center;
+
+					.profile-picture,
+					i {
+						margin-right: 10px;
+						width: 45px;
+						height: 45px;
+					}
+
+					i {
+						font-size: 30px;
+						display: flex;
+						align-items: center;
+						justify-content: center;
+					}
+				}
+
+				.report-item-summary {
+					.report-item-summary-title {
+						font-size: 14px;
+						text-transform: capitalize;
+					}
 
-			.report-item-summary {
-				font-weight: 700;
-				flex: 1;
+					.report-item-summary-description {
+						text-transform: capitalize;
+						font-size: 12px;
+					}
+				}
 			}
 
 			.report-item-actions {
 				height: 24px;
 				margin-right: 4px;
-
-				.resolve-all-icon {
-					color: var(--green);
-				}
 			}
 		}
 
@@ -493,32 +454,30 @@ export default {
 				}
 
 				&.report-sub-item-resolved {
-					.report-sub-item-description {
+					.report-sub-item-description,
+					.report-sub-item-title {
 						text-decoration: line-through;
 					}
 				}
 
-				// &.report-sub-item-unresolved {
-
-				// }
-
 				.report-sub-item-left-icon {
 					margin-right: 8px;
 					margin-top: auto;
 					margin-bottom: auto;
 				}
 
-				.report-sub-item-description {
+				.report-sub-item-info {
 					flex: 1;
 					display: flex;
 					flex-direction: column;
 
-					.report-sub-item-description-1 {
+					.report-sub-item-title {
 						font-size: 14px;
 					}
 
-					.report-sub-item-description-2 {
+					.report-sub-item-description {
 						font-size: 12px;
+						line-height: 16px;
 					}
 				}
 
@@ -527,21 +486,19 @@ export default {
 					margin-left: 8px;
 					margin-top: auto;
 					margin-bottom: auto;
-
-					i {
-						cursor: pointer;
-					}
-
-					.resolve-icon {
-						color: var(--green);
-					}
-
-					.unresolve-icon {
-						color: var(--red);
-					}
 				}
 			}
 		}
+
+		.resolve-icon {
+			color: var(--green);
+			cursor: pointer;
+		}
+
+		.unresolve-icon {
+			color: var(--red);
+			cursor: pointer;
+		}
 	}
 }
 </style>

+ 77 - 77
frontend/src/components/modals/Report.vue

@@ -10,39 +10,41 @@
 
 				<div class="columns is-multiline">
 					<div
-						v-for="issue in predefinedIssues"
+						v-for="category in predefinedCategories"
 						class="column is-half"
-						:key="issue.category"
+						:key="category.category"
 					>
-						<label class="label">{{ issue.category }}</label>
+						<label class="label">{{ category.category }}</label>
 
 						<p
-							v-for="reason in issue.reasons"
+							v-for="issue in category.issues"
 							class="control checkbox-control"
-							:key="reason.reason"
+							:key="issue.title"
 						>
 							<span class="align-horizontally">
 								<span>
 									<label class="switch">
 										<input
 											type="checkbox"
-											:id="reason.reason"
-											v-model="reason.enabled"
+											:id="issue.title"
+											v-model="issue.enabled"
 										/>
 										<span class="slider round"></span>
 									</label>
 
-									<label :for="reason.reason">
+									<label :for="issue.title">
 										<span></span>
-										<p>{{ reason.reason }}</p>
+										<p>{{ issue.title }}</p>
 									</label>
 								</span>
 
 								<i
 									class="material-icons"
-									content="Provide More Info"
+									content="Provide More info"
 									v-tippy
-									@click="reason.showInfo = !reason.showInfo"
+									@click="
+										issue.showDescription = !issue.showDescription
+									"
 								>
 									info
 								</i>
@@ -51,10 +53,10 @@
 							<input
 								type="text"
 								class="input"
-								v-model="reason.info"
-								v-if="reason.showInfo"
+								v-model="issue.description"
+								v-if="issue.showDescription"
 								placeholder="Provide more information..."
-								@keyup="reason.enabled = true"
+								@keyup="issue.enabled = true"
 							/>
 						</p>
 					</div>
@@ -169,119 +171,119 @@ export default {
 	data() {
 		return {
 			customIssues: [],
-			predefinedIssues: [
+			predefinedCategories: [
 				{
 					category: "video",
-					reasons: [
+					issues: [
 						{
 							enabled: false,
-							reason: "Doesn't exist",
-							info: "",
-							showInfo: false
+							title: "Doesn't exist",
+							description: "",
+							showDescription: false
 						},
 						{
 							enabled: false,
-							reason: "It's private",
-							info: "",
-							showInfo: false
+							title: "It's private",
+							description: "",
+							showDescription: false
 						},
 						{
 							enabled: false,
-							reason: "It's not available in my country",
-							info: "United States",
-							showInfo: false
+							title: "It's not available in my country",
+							description: "",
+							showDescription: false
 						},
 						{
 							enabled: false,
-							reason: "Unofficial",
-							info: "",
-							showInfo: false
+							title: "Unofficial",
+							description: "",
+							showDescription: false
 						}
 					]
 				},
 				{
 					category: "title",
-					reasons: [
+					issues: [
 						{
 							enabled: false,
-							reason: "Incorrect",
-							info: "",
-							showInfo: false
+							title: "Incorrect",
+							description: "",
+							showDescription: false
 						},
 						{
 							enabled: false,
-							reason: "Inappropriate",
-							info: "",
-							showInfo: false
+							title: "Inappropriate",
+							description: "",
+							showDescription: false
 						}
 					]
 				},
 				{
 					category: "duration",
-					reasons: [
+					issues: [
 						{
 							enabled: false,
-							reason: "Skips too soon",
-							info: "",
-							showInfo: false
+							title: "Skips too soon",
+							description: "",
+							showDescription: false
 						},
 						{
 							enabled: false,
-							reason: "Skips too late",
-							info: "",
-							showInfo: false
+							title: "Skips too late",
+							description: "",
+							showDescription: false
 						},
 						{
 							enabled: false,
-							reason: "Starts too soon",
-							info: "",
-							showInfo: false
+							title: "Starts too soon",
+							description: "",
+							showDescription: false
 						},
 						{
 							enabled: false,
-							reason: "Starts too late",
-							info: "",
-							showInfo: false
+							title: "Starts too late",
+							description: "",
+							showDescription: false
 						}
 					]
 				},
 				{
 					category: "artists",
-					reasons: [
+					issues: [
 						{
 							enabled: false,
-							reason: "Incorrect",
-							info: "",
-							showInfo: false
+							title: "Incorrect",
+							description: "",
+							showDescription: false
 						},
 						{
 							enabled: false,
-							reason: "Inappropriate",
-							info: "",
-							showInfo: false
+							title: "Inappropriate",
+							description: "",
+							showDescription: false
 						}
 					]
 				},
 				{
 					category: "thumbnail",
-					reasons: [
+					issues: [
 						{
 							enabled: false,
-							reason: "Incorrect",
-							info: "",
-							showInfo: false
+							title: "Incorrect",
+							description: "",
+							showDescription: false
 						},
 						{
 							enabled: false,
-							reason: "Inappropriate",
-							info: "",
-							showInfo: false
+							title: "Inappropriate",
+							description: "",
+							showDescription: false
 						},
 						{
 							enabled: false,
-							reason: "Doesn't exist",
-							info: "",
-							showInfo: false
+							title: "Doesn't exist",
+							description: "",
+							showDescription: false
 						}
 					]
 				}
@@ -304,22 +306,20 @@ export default {
 			const issues = [];
 
 			// any predefined issues that are enabled
-			this.predefinedIssues.forEach(category =>
-				category.reasons.forEach(reason => {
-					if (reason.enabled) {
-						const info =
-							reason.info === ""
-								? reason.reason
-								: `${reason.reason} - ${reason.info}`;
-
-						issues.push({ category: category.category, info });
-					}
+			this.predefinedCategories.forEach(category =>
+				category.issues.forEach(issue => {
+					if (issue.enabled)
+						issues.push({
+							category: category.category,
+							title: issue.title,
+							description: issue.description
+						});
 				})
 			);
 
 			// any custom issues
 			this.customIssues.forEach(issue =>
-				issues.push({ category: "custom", info: issue })
+				issues.push({ category: "custom", title: issue })
 			);
 
 			this.socket.dispatch(
@@ -342,7 +342,7 @@ export default {
 
 <style lang="scss">
 .edit-report-wrapper .song-item {
-	.song-info {
+	.song- {
 		width: calc(100% - 150px);
 	}
 	.thumbnail {

+ 9 - 1
frontend/src/store/modules/modals/editSong.js

@@ -32,7 +32,10 @@ export default {
 		updateSongField: ({ commit }, data) => commit("updateSongField", data),
 		selectDiscogsInfo: ({ commit }, discogsInfo) =>
 			commit("selectDiscogsInfo", discogsInfo),
-		updateReports: ({ commit }, reports) => commit("updateReports", reports)
+		updateReports: ({ commit }, reports) =>
+			commit("updateReports", reports),
+		resolveReport: ({ commit }, reportId) =>
+			commit("resolveReport", reportId)
 	},
 	mutations: {
 		showTab(state, tab) {
@@ -78,6 +81,11 @@ export default {
 		},
 		updateReports(state, reports) {
 			state.reports = reports;
+		},
+		resolveReport(state, reportId) {
+			state.reports = state.reports.filter(
+				report => report._id !== reportId
+			);
 		}
 	}
 };