Compare commits

...

14 Commits

Author SHA1 Message Date
a6d910f56a sort feedback desc
All checks were successful
delpoy / build-and-deploy (push) Successful in 51s
2024-12-29 00:19:24 +01:00
fde50d21a6 fuck that shit, revert
All checks were successful
delpoy / build-and-deploy (push) Successful in 1m3s
2024-12-28 03:46:17 +01:00
8ea1750f1a fix scroll position resetting on admin panel
All checks were successful
delpoy / build-and-deploy (push) Successful in 1m2s
2024-12-28 02:26:43 +01:00
5935b0d561 stick header and filter bar to top
All checks were successful
delpoy / build-and-deploy (push) Successful in 48s
2024-12-28 01:29:05 +01:00
6e7c2eafca fix reported user not updating when changed
All checks were successful
delpoy / build-and-deploy (push) Successful in 45s
2024-12-28 00:51:36 +01:00
3c8dc30e43 increase string size for some database fields
All checks were successful
delpoy / build-and-deploy (push) Successful in 48s
2024-12-28 00:44:12 +01:00
8d8b1c52c0 make feedback title optional
All checks were successful
delpoy / build-and-deploy (push) Successful in 54s
2024-12-27 19:00:22 +01:00
1596fb605e update report admin endpoint
All checks were successful
delpoy / build-and-deploy (push) Successful in 56s
2024-12-24 01:18:36 +01:00
7357ad9e88 actually fix strike date not set if status changed but strike reason not
All checks were successful
delpoy / build-and-deploy (push) Successful in 39s
2024-12-20 21:07:36 +01:00
3dd56bc471 fix strike date not set if status changed but strike reason not
All checks were successful
delpoy / build-and-deploy (push) Successful in 40s
2024-12-20 20:56:38 +01:00
8e60f83b6f fix crashing webhook sending 2024-12-20 20:46:48 +01:00
0280e2a277 fix report details not showing after report is submitted
All checks were successful
delpoy / build-and-deploy (push) Successful in 36s
2024-12-20 19:24:59 +01:00
60f031aa7b update input disabled style
All checks were successful
delpoy / build-and-deploy (push) Successful in 1m5s
2024-12-20 19:10:52 +01:00
e7bba22784 update report submitted window (#42)
All checks were successful
delpoy / build-and-deploy (push) Successful in 49s
2024-12-06 15:00:05 +01:00
21 changed files with 265 additions and 168 deletions

View File

@ -98,6 +98,7 @@
class:input-lg={type !== 'checkbox' && size === 'lg'} class:input-lg={type !== 'checkbox' && size === 'lg'}
class:input-bordered={type !== 'checkbox'} class:input-bordered={type !== 'checkbox'}
class:pr-11={initialType === 'password'} class:pr-11={initialType === 'password'}
class:!border-none,!text-inherit={disabled}
{id} {id}
{name} {name}
{type} {type}

View File

@ -46,6 +46,7 @@
class:select-sm={size === 'sm'} class:select-sm={size === 'sm'}
class:select-md={size === 'md'} class:select-md={size === 'md'}
class:select-lg={size === 'lg'} class:select-lg={size === 'lg'}
class:!border-none,!text-inherit={disabled}
{id} {id}
{name} {name}
{required} {required}

View File

@ -1,18 +1,27 @@
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte'; let {
id,
export let id: string | null = null; name,
export let name: string | null = null; value = $bindable(),
export let value: string | null = null; label,
export let label: string | null = null; notice,
export let notice: string | null = null; required,
export let required = false; disabled,
export let disabled = false; readonly,
export let readonly = false; size = 'md',
export let size: 'xs' | 'sm' | 'md' | 'lg' = 'md'; rows = 2
export let rows = 2; }: {
id?: string;
const dispatch = createEventDispatcher(); name?: string;
value?: string;
label?: string;
notice?: string;
required?: boolean;
disabled?: boolean;
readonly?: boolean;
size?: 'xs' | 'sm' | 'md' | 'lg';
rows?: number;
} = $props();
</script> </script>
<div> <div>
@ -27,12 +36,12 @@
</label> </label>
{/if} {/if}
<textarea <textarea
class="textarea w-full" class="textarea textarea-bordered w-full"
class:textarea-xs={size === 'xs'} class:textarea-xs={size === 'xs'}
class:textarea-sm={size === 'sm'} class:textarea-sm={size === 'sm'}
class:textarea-md={size === 'md'} class:textarea-md={size === 'md'}
class:textarea-lg={size === 'lg'} class:textarea-lg={size === 'lg'}
class:textarea-bordered={!readonly} class:!border-none,!text-inherit={disabled}
{id} {id}
{name} {name}
{required} {required}
@ -40,7 +49,6 @@
{readonly} {readonly}
{rows} {rows}
bind:value bind:value
on:click={(e) => dispatch('click', e)}
></textarea> ></textarea>
{#if notice} {#if notice}
<label class="label" for={id}> <label class="label" for={id}>

View File

@ -24,16 +24,16 @@ export class User extends Model {
@Column({ type: DataTypes.DATE, allowNull: false }) @Column({ type: DataTypes.DATE, allowNull: false })
declare birthday: Date; declare birthday: Date;
@Column({ type: DataTypes.STRING }) @Column({ type: DataTypes.STRING })
declare telephone: string; declare telephone: string | null;
@Column({ type: DataTypes.STRING, allowNull: false }) @Column({ type: DataTypes.STRING, allowNull: false })
declare username: string; declare username: string;
@Column({ type: DataTypes.ENUM('java', 'bedrock', 'noauth'), allowNull: false }) @Column({ type: DataTypes.ENUM('java', 'bedrock', 'noauth'), allowNull: false })
declare playertype: 'java' | 'bedrock' | 'noauth'; declare playertype: 'java' | 'bedrock' | 'noauth';
@Column({ type: DataTypes.STRING }) @Column({ type: DataTypes.STRING })
declare password: string; declare password: string | null;
@Column({ type: DataTypes.UUID, unique: true }) @Column({ type: DataTypes.UUID, unique: true })
@Index @Index
declare uuid: string; declare uuid: string | null;
} }
@Table({ modelName: 'report', underscored: true }) @Table({ modelName: 'report', underscored: true })
@ -43,16 +43,16 @@ export class Report extends Model {
declare url_hash: string; declare url_hash: string;
@Column({ type: DataTypes.STRING, allowNull: false }) @Column({ type: DataTypes.STRING, allowNull: false })
declare subject: string; declare subject: string;
@Column({ type: DataTypes.STRING }) @Column({ type: DataTypes.TEXT })
declare body: string; declare body: string | null;
@Column({ type: DataTypes.BOOLEAN, allowNull: false }) @Column({ type: DataTypes.BOOLEAN, allowNull: false })
declare draft: boolean; declare draft: boolean;
@Column({ type: DataTypes.ENUM('none', 'review', 'reviewed'), allowNull: false }) @Column({ type: DataTypes.ENUM('none', 'review', 'reviewed'), allowNull: false })
declare status: 'none' | 'review' | 'reviewed'; declare status: 'none' | 'review' | 'reviewed';
@Column({ type: DataTypes.STRING }) @Column({ type: DataTypes.STRING })
declare notice: string; declare notice: string | null;
@Column({ type: DataTypes.STRING }) @Column({ type: DataTypes.TEXT })
declare statement: string; declare statement: string | null;
@Column({ type: DataTypes.DATE }) @Column({ type: DataTypes.DATE })
declare striked_at: Date | null; declare striked_at: Date | null;
@Column({ type: DataTypes.INTEGER, allowNull: false }) @Column({ type: DataTypes.INTEGER, allowNull: false })
@ -60,10 +60,10 @@ export class Report extends Model {
declare reporter_id: number; declare reporter_id: number;
@Column({ type: DataTypes.INTEGER }) @Column({ type: DataTypes.INTEGER })
@ForeignKey(() => User) @ForeignKey(() => User)
declare reported_id: number; declare reported_id: number | null;
@Column({ type: DataTypes.INTEGER }) @Column({ type: DataTypes.INTEGER })
@ForeignKey(() => Admin) @ForeignKey(() => Admin)
declare auditor_id: number; declare auditor_id: number | null;
@Column({ type: DataTypes.INTEGER }) @Column({ type: DataTypes.INTEGER })
@ForeignKey(() => StrikeReason) @ForeignKey(() => StrikeReason)
declare strike_reason_id: number | null; declare strike_reason_id: number | null;
@ -72,22 +72,22 @@ export class Report extends Model {
onDelete: 'CASCADE', onDelete: 'CASCADE',
foreignKey: 'reporter_id' foreignKey: 'reporter_id'
}) })
declare reporter: User; declare reporter: User | null;
@BelongsTo(() => User, { @BelongsTo(() => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
foreignKey: 'reported_id' foreignKey: 'reported_id'
}) })
declare reported: User; declare reported: User | null;
@BelongsTo(() => Admin, { @BelongsTo(() => Admin, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
foreignKey: 'auditor_id' foreignKey: 'auditor_id'
}) })
declare auditor: Admin; declare auditor: Admin | null;
@BelongsTo(() => StrikeReason, { @BelongsTo(() => StrikeReason, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
foreignKey: 'strike_reason_id' foreignKey: 'strike_reason_id'
}) })
declare strike_reason: StrikeReason; declare strike_reason: StrikeReason | null;
} }
@Table({ modelName: 'strike_reason', underscored: true, createdAt: false, updatedAt: false }) @Table({ modelName: 'strike_reason', underscored: true, createdAt: false, updatedAt: false })
@ -113,19 +113,21 @@ export class Feedback extends Model {
@Column({ type: DataTypes.STRING, allowNull: false }) @Column({ type: DataTypes.STRING, allowNull: false })
declare event: string; declare event: string;
@Column({ type: DataTypes.STRING }) @Column({ type: DataTypes.STRING })
declare content: string; declare title: string | null;
@Column({ type: DataTypes.TEXT })
declare content: string | null;
@Column({ type: DataTypes.STRING, allowNull: false, unique: true }) @Column({ type: DataTypes.STRING, allowNull: false, unique: true })
@Index @Index
declare url_hash: string; declare url_hash: string;
@Column({ type: DataTypes.INTEGER }) @Column({ type: DataTypes.INTEGER })
@ForeignKey(() => User) @ForeignKey(() => User)
declare user_id: number; declare user_id: number | null;
@BelongsTo(() => User, { @BelongsTo(() => User, {
onDelete: 'CASCADE', onDelete: 'CASCADE',
foreignKey: 'user_id' foreignKey: 'user_id'
}) })
declare user: User; declare user: User | null;
} }
@Table({ modelName: 'admin', underscored: true }) @Table({ modelName: 'admin', underscored: true })

View File

@ -82,7 +82,7 @@
let pageTitleSuffix = $derived( let pageTitleSuffix = $derived(
tabs.find((t) => $page.url.pathname === t.path)?.name ?? tabs.find((t) => $page.url.pathname === t.path)?.name ??
($page.url.pathname === `${env.PUBLIC_BASE_PATH}/admin/login` ? 'Login' : null) ($page.url.pathname === `${env.PUBLIC_BASE_PATH}/admin/login` ? 'Login' : null)
); );
</script> </script>

View File

@ -68,12 +68,13 @@
<hr class="divider my-1 mx-8 border-none" /> <hr class="divider my-1 mx-8 border-none" />
<table class="table table-fixed h-fit"> <table class="table table-fixed h-fit">
<thead> <thead>
<tr> <tr>
<th>Event</th> <th>Event</th>
<th>Nutzer</th> <th>Titel</th>
<th>Datum</th> <th>Nutzer</th>
<th>Inhalt</th> <th>Datum</th>
</tr> <th>Inhalt</th>
</tr>
</thead> </thead>
<PaginationTableBody <PaginationTableBody
onUpdate={async () => onUpdate={async () =>
@ -91,6 +92,7 @@
}} }}
> >
<td title={feedback.event}>{feedback.event}</td> <td title={feedback.event}>{feedback.event}</td>
<td class="overflow-hidden overflow-ellipsis">{feedback.title}</td>
<td class="flex"> <td class="flex">
{feedback.user?.username || ''} {feedback.user?.username || ''}
{#if feedback.user} {#if feedback.user}
@ -108,15 +110,17 @@
{/if} {/if}
</td> </td>
<td <td
>{new Intl.DateTimeFormat('de-DE', { >{new Intl.DateTimeFormat('de-DE', {
year: 'numeric', year: 'numeric',
month: '2-digit', month: '2-digit',
day: '2-digit', day: '2-digit',
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'
}).format(new Date(feedback.updatedAt))} Uhr</td }).format(new Date(feedback.updatedAt))} Uhr</td
>
<td class="overflow-hidden overflow-ellipsis"
>{feedback.content}{feedback.content_stripped ? '...' : ''}</td
> >
<td>{feedback.content}...</td>
</tr> </tr>
{/each} {/each}
</PaginationTableBody> </PaginationTableBody>
@ -167,6 +171,18 @@
</div> </div>
<h3 class="font-roboto font-semibold text-2xl mb-2">Feedback</h3> <h3 class="font-roboto font-semibold text-2xl mb-2">Feedback</h3>
<div class="w-full"> <div class="w-full">
<Input readonly={true} size="sm" value={activeFeedback.event} pickyWidth={false}>
{#snippet label()}
<span>Event</span>
{/snippet}
</Input>
<Input readonly={true} size="sm" value={activeFeedback.title} pickyWidth={false}>
{#snippet label()}
<span>Titel</span>
{/snippet}
</Input>
<Textarea readonly={true} rows={4} label="Inhalt" value={activeFeedback.content} />
<div class="divider mb-1"></div>
<Input <Input
readonly={true} readonly={true}
size="sm" size="sm"
@ -177,7 +193,6 @@
<span>Nutzer</span> <span>Nutzer</span>
{/snippet} {/snippet}
</Input> </Input>
<Textarea readonly={true} rows={4} label="Inhalt" value={activeFeedback.content} />
</div> </div>
</div> </div>
{/if} {/if}

View File

@ -42,10 +42,14 @@ export const POST = (async ({ request, cookies }) => {
attributes: data.preview attributes: data.preview
? { ? {
exclude: ['content'], exclude: ['content'],
include: [[sequelize.literal('SUBSTR(content, 1, 50)'), 'content']] include: [
[sequelize.literal('SUBSTR(content, 1, 50)'), 'content'],
[sequelize.literal('LENGTH(content) > 50'), 'content_stripped']
]
} }
: undefined, : undefined,
include: { model: User, as: 'user' }, include: { model: User, as: 'user' },
order: [['created_at', 'DESC']],
offset: data.hash ? 0 : data.from || 0, offset: data.hash ? 0 : data.from || 0,
limit: data.hash ? 1 : data.limit || 100 limit: data.hash ? 1 : data.limit || 100
}); });

View File

@ -125,14 +125,14 @@
<col style="width: 15%" /> <col style="width: 15%" />
</colgroup> </colgroup>
<thead> <thead>
<tr> <tr>
<th>Grund</th> <th>Grund</th>
<th>Ersteller</th> <th>Ersteller</th>
<th>Reporteter User</th> <th>Reporteter User</th>
<th>Datum</th> <th>Datum</th>
<th>Bearbeitungsstatus</th> <th>Bearbeitungsstatus</th>
<th>Reportstatus</th> <th>Reportstatus</th>
</tr> </tr>
</thead> </thead>
<PaginationTableBody <PaginationTableBody
onUpdate={async () => onUpdate={async () =>
@ -146,7 +146,7 @@
goto(`${window.location.href.split('#')[0]}#${report.url_hash}`, { goto(`${window.location.href.split('#')[0]}#${report.url_hash}`, {
replaceState: true replaceState: true
}); });
activeReport = report; activeReport = $state.snapshot(report);
activeReport.originalStatus = report.status; activeReport.originalStatus = report.status;
}} }}
> >
@ -182,13 +182,13 @@
{/if} {/if}
</td> </td>
<td <td
>{new Intl.DateTimeFormat('de-DE', { >{new Intl.DateTimeFormat('de-DE', {
year: 'numeric', year: 'numeric',
month: '2-digit', month: '2-digit',
day: '2-digit', day: '2-digit',
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'
}).format(new Date(report.createdAt))} Uhr</td }).format(new Date(report.createdAt))} Uhr</td
> >
<td> <td>
{report.status === 'none' {report.status === 'none'
@ -305,7 +305,7 @@
<option <option
value="none" value="none"
disabled={activeReport.auditor != null || activeReport.notice || activeReport.statement} disabled={activeReport.auditor != null || activeReport.notice || activeReport.statement}
>Unbearbeitet</option >Unbearbeitet</option
> >
<option value="review">In Bearbeitung</option> <option value="review">In Bearbeitung</option>
<option value="reviewed">Bearbeitet</option> <option value="reviewed">Bearbeitet</option>
@ -344,7 +344,13 @@
} else { } else {
activeReport.reported = undefined; activeReport.reported = undefined;
} }
reports = [...reports]; const activeReportIndex = reports.findIndex((r) => r.id === activeReport.id);
if (activeReportIndex === -1) {
return;
}
reports[activeReportIndex] = activeReport;
if ( if (
activeReport.originalStatus !== 'reviewed' && activeReport.originalStatus !== 'reviewed' &&
activeReport.status === 'reviewed' activeReport.status === 'reviewed'
@ -373,7 +379,7 @@
onSubmit={(e) => { onSubmit={(e) => {
if (!e.draft) $reportCount += 1; if (!e.draft) $reportCount += 1;
reports = [e, ...reports]; reports = [e, ...reports];
activeReport = reports[0]; activeReport = $state.snapshot(reports[0]);
newReportModal.close(); newReportModal.close();
}} }}
/> />

View File

@ -50,12 +50,7 @@ export const POST = (async ({ request, cookies }) => {
let reports = await Report.findAll({ let reports = await Report.findAll({
where: reportFindOptions, where: reportFindOptions,
include: [ include: [{ all: true }],
{ model: User, as: 'reporter' },
{ model: User, as: 'reported' },
{ model: Admin, as: 'auditor' },
{ model: StrikeReason, as: 'strike_reason' }
],
order: [['created_at', 'DESC']], order: [['created_at', 'DESC']],
offset: data.from || 0, offset: data.from || 0,
limit: data.limit || 100 limit: data.limit || 100
@ -102,64 +97,61 @@ export const PATCH = (async ({ request, cookies }) => {
} }
const data = parseResult.data; const data = parseResult.data;
const report = await Report.findOne({ where: { id: data.id } }); const report = await Report.findOne({ where: { id: data.id }, include: [{ all: true }] });
const admin = await Admin.findOne({ where: { id: data.auditor } }); const admin = await Admin.findOne({ where: { id: data.auditor } });
const reported = data.reported const reported = data.reported ? await User.findOne({ where: { uuid: data.reported } }) : null;
? await User.findOne({ where: { uuid: data.reported } }) if (report === null || (admin === null && data.auditor != -1))
: undefined;
if (report === null || (admin === null && data.auditor != -1) || reported === null)
return new Response(null, { status: 400 }); return new Response(null, { status: 400 });
const webhookTriggerUsers: string[] = []; const webhookTriggerUsers: string[] = [];
if (report.reported_id != reported?.id) {
const oldReportUser = await User.findByPk(report.reported_id); // check if strike reason has changed and return 400 if it doesn't exist
if (oldReportUser) webhookTriggerUsers.push(oldReportUser.uuid); if (
if (reported) webhookTriggerUsers.push(reported.uuid); (report.strike_reason?.id ?? -1) != data.strike_reason &&
} else if ( data.strike_reason != null &&
reported && data.strike_reason != -1
report.reported_id != null &&
report.strike_reason_id != data.strike_reason
) { ) {
webhookTriggerUsers.push(reported.uuid); const strike_reason = await StrikeReason.findByPk(data.strike_reason);
if (strike_reason == null) return new Response(null, { status: 400 });
}
if (data.status === 'reviewed') {
// trigger webhook if status changed to reviewed
if (report.status !== 'reviewed' && data.strike_reason != -1 && reported) {
webhookTriggerUsers.push(reported.uuid!);
}
// trigger webhook if strike reason has changed
else if (
(report.strike_reason?.id ?? -1) != data.strike_reason &&
report.reported &&
reported
) {
webhookTriggerUsers.push(reported.uuid!);
}
} else if (report.status === 'reviewed') {
// trigger webhook if report status is reviewed and reported user has changed
if (report.strike_reason != null && report.reported) {
webhookTriggerUsers.push(report.reported.uuid!);
}
} }
report.reported_id = reported?.id ?? null;
if (data.notice != null) report.notice = data.notice; if (data.notice != null) report.notice = data.notice;
if (data.statement != null) report.statement = data.statement; if (data.statement != null) report.statement = data.statement;
if (data.status != null) report.status = data.status; if (data.status != null) report.status = data.status;
if (data.strike_reason != null) { if (data.reported != null && reported) report.reported_id = reported.id;
if (data.status !== 'reviewed') { if (data.strike_reason != null)
if (data.strike_reason == -1) { report.strike_reason_id = data.strike_reason == -1 ? null : data.strike_reason;
report.strike_reason_id = null; if (data.strike_reason != null)
} else { report.striked_at = data.strike_reason == -1 ? null : new Date(Date.now());
const strike_reason = await StrikeReason.findByPk(data.strike_reason);
if (strike_reason == null) return new Response(null, { status: 400 });
report.strike_reason_id = strike_reason.id;
}
} else if (data.strike_reason == -1 && report.strike_reason_id != null) {
report.strike_reason_id = null;
report.striked_at = null;
} else if (data.strike_reason != -1 && data.strike_reason != report.strike_reason_id) {
if (!report.reported_id) return new Response(null, { status: 400 });
const strike_reason = await StrikeReason.findByPk(data.strike_reason);
if (strike_reason == null) return new Response(null, { status: 400 });
report.strike_reason_id = strike_reason.id;
report.striked_at = new Date(Date.now());
}
}
if (admin != null) report.auditor_id = admin.id; if (admin != null) report.auditor_id = admin.id;
await report.save(); await report.save();
if (webhookTriggerUsers.length > 0 && data.status == 'reviewed' && env.REPORTED_WEBHOOK) { for (const webhookTriggerUser of webhookTriggerUsers) {
for (const webhookTriggerUser of webhookTriggerUsers) { // no `await` to avoid blocking
try { webhookUserReported(env.REPORTED_WEBHOOK, webhookTriggerUser).catch((e) =>
// no `await` to avoid blocking console.error(`failed to send reported webhook: ${e}`)
webhookUserReported(env.REPORTED_WEBHOOK, webhookTriggerUser); );
} catch (e) {
console.error(`failed to send reported webhook: ${e}`);
}
}
} }
return new Response(); return new Response();

View File

@ -95,19 +95,19 @@
<div class="h-full overflow-scroll" bind:this={userTableContainerElement}> <div class="h-full overflow-scroll" bind:this={userTableContainerElement}>
<table class="table table-auto"> <table class="table table-auto">
<thead> <thead>
<!-- prettier-ignore --> <!-- prettier-ignore -->
<SortableTr class="[&>th]:bg-base-100 [&>th]:z-[1] [&>th]:sticky [&>th]:top-0"> <SortableTr class="[&>th]:bg-base-100 [&>th]:z-[1] [&>th]:sticky [&>th]:top-0">
<th></th> <th></th>
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'firstname', asc: e.asc}}}>Vorname</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'firstname', asc: e.asc}}}>Vorname</SortableTh>
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'lastname', asc: e.asc}}}>Nachname</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'lastname', asc: e.asc}}}>Nachname</SortableTh>
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'birthday', asc: e.asc}}}>Geburtstag</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'birthday', asc: e.asc}}}>Geburtstag</SortableTh>
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'telephone', asc: e.asc}}}>Telefon</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'telephone', asc: e.asc}}}>Telefon</SortableTh>
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'username', asc: e.asc}}}>Username</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'username', asc: e.asc}}}>Username</SortableTh>
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'playertype', asc: e.asc}}}>Minecraft Edition</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'playertype', asc: e.asc}}}>Minecraft Edition</SortableTh>
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'password', asc: e.asc}}}>Passwort</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'password', asc: e.asc}}}>Passwort</SortableTh>
<SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'uuid', asc: e.asc}}}>UUID</SortableTh> <SortableTh onSort={(e) => userFilter = {...userFilter, sort: {key: 'uuid', asc: e.asc}}}>UUID</SortableTh>
<th></th> <th></th>
</SortableTr> </SortableTr>
</thead> </thead>
<PaginationTableBody <PaginationTableBody
onUpdate={async () => { onUpdate={async () => {

View File

@ -21,9 +21,10 @@ export const POST = (async ({ request, url }) => {
where: { uuid: data.users }, where: { uuid: data.users },
attributes: ['id', 'uuid'] attributes: ['id', 'uuid']
})) { })) {
feedback[user.uuid] = { feedback[user.uuid!] = {
url_hash: crypto.randomBytes(18).toString('hex'), url_hash: crypto.randomBytes(18).toString('hex'),
event: data.event, event: data.event,
title: data.title ?? null,
draft: true, draft: true,
user_id: user.id user_id: user.id
}; };

View File

@ -2,5 +2,6 @@ import { z } from 'zod';
export const FeedbackAddSchema = z.object({ export const FeedbackAddSchema = z.object({
event: z.string(), event: z.string(),
title: z.string().nullish(),
users: z.array(z.string()) users: z.array(z.string())
}); });

View File

@ -12,7 +12,7 @@ export const load: PageServerLoad = async ({ params }) => {
return { return {
draft: feedback.content === null, draft: feedback.content === null,
event: feedback.event, title: feedback.title,
anonymous: feedback.user_id === null anonymous: feedback.user_id === null
}; };
}; };

View File

@ -16,7 +16,7 @@
{#if draft} {#if draft}
<div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}> <div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}>
<FeedbackDraft event={data.event} anonymous={data.anonymous} onSubmit={() => (draft = false)} /> <FeedbackDraft title={data.title} anonymous={data.anonymous} onSubmit={() => (draft = false)} />
</div> </div>
{:else} {:else}
<div class="col-[1] row-[1]" transition:fly={{ x: 200, duration: 300 }}> <div class="col-[1] row-[1]" transition:fly={{ x: 200, duration: 300 }}>

View File

@ -5,8 +5,11 @@
import { page } from '$app/stores'; import { page } from '$app/stores';
import { getPopupModalShowFn } from '$lib/context'; import { getPopupModalShowFn } from '$lib/context';
let { event, anonymous, onSubmit }: { event: string; anonymous: boolean; onSubmit?: () => void } = let {
$props(); title,
anonymous,
onSubmit
}: { title: string | null; anonymous: boolean; onSubmit?: () => void } = $props();
let showPopupModal = getPopupModalShowFn(); let showPopupModal = getPopupModalShowFn();
@ -47,11 +50,13 @@
}} }}
> >
<div class="space-y-4 my-4"> <div class="space-y-4 my-4">
<Input size="sm" pickyWidth={false} disabled value={event}> {#if title}
{#snippet label()} <Input size="sm" pickyWidth={false} disabled value={title}>
<span>Event</span> {#snippet label()}
{/snippet} <span>Event</span>
</Input> {/snippet}
</Input>
{/if}
<Textarea required={true} rows={4} label="Feedback" bind:value={content} /> <Textarea required={true} rows={4} label="Feedback" bind:value={content} />
{#if !anonymous} {#if !anonymous}
<div class="flex items-center gap-2 mt-2"> <div class="flex items-center gap-2 mt-2">

View File

@ -82,27 +82,27 @@
<div class="divider"></div> <div class="divider"></div>
<div class="flex justify-around mt-2 mb-4"> <div class="flex justify-around mt-2 mb-4">
<div class="grid grid-cols-1 sm:grid-cols-2 w-full sm:w-fit gap-x-4 gap-y-2"> <div class="grid grid-cols-1 sm:grid-cols-2 w-full sm:w-fit gap-x-4 gap-y-2">
<Input value={firstname} size="sm" readonly> <Input value={firstname} size="sm" disabled>
{#snippet label()} {#snippet label()}
<span>Vorname</span> <span>Vorname</span>
{/snippet} {/snippet}
</Input> </Input>
<Input value={lastname} size="sm" readonly> <Input value={lastname} size="sm" disabled>
{#snippet label()} {#snippet label()}
<span>Nachname</span> <span>Nachname</span>
{/snippet} {/snippet}
</Input> </Input>
<Input value={birthday.toISOString().substring(0, 10)} type="date" size="sm" readonly> <Input value={birthday.toISOString().substring(0, 10)} type="date" size="sm" disabled>
{#snippet label()} {#snippet label()}
<span>Geburtstag</span> <span>Geburtstag</span>
{/snippet}</Input {/snippet}</Input
> >
<Input value={phone} size="sm" readonly> <Input value={phone} size="sm" disabled>
{#snippet label()} {#snippet label()}
<span>Telefonnummer</span> <span>Telefonnummer</span>
{/snippet} {/snippet}
</Input> </Input>
<Input value={username} size="sm" readonly> <Input value={username} size="sm" disabled>
{#snippet label()} {#snippet label()}
<span>Spielername</span> <span>Spielername</span>
{/snippet} {/snippet}

View File

@ -18,6 +18,8 @@ export const load: PageServerLoad = async ({ params }) => {
draft: report.draft, draft: report.draft,
status: report.status, status: report.status,
reason: report.subject, reason: report.subject,
body: report.body,
statement: report.statement,
reporter: { reporter: {
name: report.reporter.username name: report.reporter.username
}, },

View File

@ -1,11 +1,14 @@
<script lang="ts"> <script lang="ts">
import { fly } from 'svelte/transition'; import { fly } from 'svelte/transition';
import ReportDraft from './ReportDraft.svelte'; import ReportDraft from './ReportDraft.svelte';
import ReportCompleted from './ReportCompleted.svelte';
import ReportSubmitted from './ReportSubmitted.svelte'; import ReportSubmitted from './ReportSubmitted.svelte';
let { data } = $props(); let { data } = $props();
let reason = $state(data.reason);
let body = $state(data.body);
let reporterName = $state(data.reporter.name);
let reportedName = $state(data.reported.name || null);
let completed = $state(!data.draft); let completed = $state(!data.draft);
</script> </script>
@ -19,18 +22,24 @@
{#if !completed} {#if !completed}
<div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}> <div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}>
<ReportDraft <ReportDraft
reason={data.reason} bind:reason
reporterName={data.reporter.name} bind:body
reportedName={data.reported.name ?? null} bind:reporterName
bind:reportedName
users={data.users ?? []} users={data.users ?? []}
onsubmit={() => (completed = true)} onsubmit={() => (completed = true)}
/> />
</div> </div>
{:else if data.status === 'reviewed'}
<ReportCompleted />
{:else} {:else}
<div class="col-[1] row-[1]" transition:fly={{ x: 200, duration: 300 }}> <div class="col-[1] row-[1]" transition:fly={{ x: 200, duration: 300 }}>
<ReportSubmitted /> <ReportSubmitted
{reason}
{body}
{reporterName}
{reportedName}
status={data.status}
statement={data.statement}
/>
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -1,3 +0,0 @@
<div>
<h2 class="text-xl text-center">Dieser Report wurde von einem Admin bearbeitet</h2>
</div>

View File

@ -8,15 +8,17 @@
import { getPopupModalShowFn } from '$lib/context'; import { getPopupModalShowFn } from '$lib/context';
let { let {
reporterName, reporterName = $bindable(),
reportedName = null, reportedName = $bindable(null),
reason, reason = $bindable(),
body = $bindable(),
users, users,
onsubmit onsubmit
}: { }: {
reporterName: string; reporterName: string;
reportedName: string | null; reportedName: string | null;
reason: string; reason: string;
body: string;
users: string[]; users: string[];
onsubmit: () => void; onsubmit: () => void;
} = $props(); } = $props();
@ -24,7 +26,6 @@
let showPopupModal = getPopupModalShowFn(); let showPopupModal = getPopupModalShowFn();
let reported = $state(reportedName); let reported = $state(reportedName);
let content = $state('');
async function submitReport() { async function submitReport() {
await fetch(`${env.PUBLIC_BASE_PATH}/report/${$page.params.url_hash}`, { await fetch(`${env.PUBLIC_BASE_PATH}/report/${$page.params.url_hash}`, {
@ -32,7 +33,7 @@
body: JSON.stringify({ body: JSON.stringify({
reported: reported || null, reported: reported || null,
subject: reason, subject: reason,
body: content body: body
}) })
}); });
} }
@ -108,7 +109,7 @@
required={true} required={true}
rows={4} rows={4}
label="Details über den Report Grund" label="Details über den Report Grund"
bind:value={content} bind:value={body}
/> />
</div> </div>
</div> </div>

View File

@ -1,6 +1,58 @@
<script lang="ts">
import Input from '$lib/components/Input/Input.svelte';
import Textarea from '$lib/components/Input/Textarea.svelte';
let {
reporterName,
reportedName,
status,
reason,
body,
statement
}: {
reporterName: string;
reportedName: string | null;
status: 'none' | 'review' | 'reviewed';
reason: string;
body: string;
statement?: string;
} = $props();
</script>
<div> <div>
<h2 class="text-2xl text-center">Report abgeschickt</h2> <h2 class="text-3xl text-center">
<p class="mt-4"> Report von <span class="underline">{reporterName}</span> gegen
Dein Report wurde abgeschickt und wird so schnell wie möglich von einem Admin bearbeitet. <span class="underline">{reportedName ?? 'unbekannt'}</span>
</p> </h2>
<h4 class="text-xl text-center mt-1 mb-0">
{#if status === 'none'}
<span>Unbearbeitet</span>
{:else if status === 'review'}
<span>In Bearbeitung</span>
{:else if status === 'reviewed'}
<span>Bearbeitet</span>
{/if}
<span class="relative inline-flex h-3 w-3 ml-[1px]">
{#if status === 'review'}
<span
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-yellow-500 opacity-75"
></span>
{/if}
<span
class="relative inline-flex rounded-full h-3 w-3"
class:bg-yellow-500={status === 'none' || status === 'review'}
class:bg-green-500={status === 'reviewed'}
></span>
</span>
</h4>
<div class="my-4">
<Input type="text" size="sm" value={reason} disabled={true} pickyWidth={false}>
{#snippet label()}
<span>Report Grund</span>
{/snippet}
</Input>
<Textarea disabled={true} rows={4} value={body} label="Report Details" />
</div>
<div class="divider divider-vertical mb-1"></div>
<Textarea disabled={true} rows={4} label="Admin Kommentar" value={statement} />
</div> </div>