Implement event stuff
parent
e11bcc2a08
commit
e1c6a7bb14
|
@ -66,6 +66,35 @@
|
|||
--vs-dropdown-option--active-bg: var(--accent)!important;
|
||||
--vs-dropdown-option--active-color: var(--base)!important;
|
||||
--vs-border-color: var(--overlay-0)!important;
|
||||
|
||||
/*
|
||||
--rosewater: #f4dbd6;
|
||||
--flamingo: #f0c6c6;
|
||||
--pink: #f5bde6;
|
||||
--mauve: #c6a0f6;
|
||||
--red: #ed8796;
|
||||
--maroon: #ee99a0;
|
||||
--peach: #f5a97f;
|
||||
--yellow: #eed49f;
|
||||
--green: #a6da95;
|
||||
--teal: #8bd5ca;
|
||||
--sky: #91d7e3;
|
||||
--sapphire: #7dc4e4;
|
||||
--blue: #8aadf4;
|
||||
--lavender: #b7bdf8;
|
||||
--text: #cad3f5;
|
||||
--subtext-1: #b8c0e0;
|
||||
--subtext-0: #a5adcb;
|
||||
--overlay-2: #939ab7;
|
||||
--overlay-1: #8087a2;
|
||||
--overlay-0: #6e738d;
|
||||
--surface-2: #5b6078;
|
||||
--surface-1: #494d64;
|
||||
--surface-0: #363a4f;
|
||||
--base: #24273a;
|
||||
--mantle: #1e2030;
|
||||
--crust: #181926;
|
||||
*/
|
||||
}
|
||||
|
||||
/* semantic color variables for this project */
|
||||
|
|
|
@ -17,6 +17,9 @@ export type { CreateTeamJson } from './models/CreateTeamJson';
|
|||
export type { EditMemberRolesJson } from './models/EditMemberRolesJson';
|
||||
export type { EventSchema } from './models/EventSchema';
|
||||
export type { EventSchemaList } from './models/EventSchemaList';
|
||||
export type { GetEventPlayersResponse } from './models/GetEventPlayersResponse';
|
||||
export type { PlayerEventRolesSchema } from './models/PlayerEventRolesSchema';
|
||||
export type { PlayerRoleSchema } from './models/PlayerRoleSchema';
|
||||
export type { PlayerSchema } from './models/PlayerSchema';
|
||||
export type { PlayerTeamAvailabilityRoleSchema } from './models/PlayerTeamAvailabilityRoleSchema';
|
||||
export type { PutScheduleForm } from './models/PutScheduleForm';
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PlayerRoleSchema } from './PlayerRoleSchema';
|
||||
export type CreateEventJson = {
|
||||
description: string;
|
||||
name: string;
|
||||
playerIds: Array<number>;
|
||||
playerRoles: Array<PlayerRoleSchema>;
|
||||
startTime: string;
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type CreateTeamJson = {
|
||||
discordWebhookUrl?: string;
|
||||
discordWebhookUrl?: (string | null);
|
||||
leagueTimezone: string;
|
||||
minuteOffset?: number;
|
||||
teamName: string;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
/* eslint-disable */
|
||||
export type EventSchema = {
|
||||
createdAt: string;
|
||||
description?: string;
|
||||
description: (string | null);
|
||||
id: number;
|
||||
name: string;
|
||||
startTime: string;
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PlayerEventRolesSchema } from './PlayerEventRolesSchema';
|
||||
export type GetEventPlayersResponse = {
|
||||
players: Array<PlayerEventRolesSchema>;
|
||||
};
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PlayerSchema } from './PlayerSchema';
|
||||
import type { RoleSchema } from './RoleSchema';
|
||||
export type PlayerEventRolesSchema = {
|
||||
hasConfirmed: boolean;
|
||||
player: PlayerSchema;
|
||||
playtime: number;
|
||||
role: (RoleSchema | null);
|
||||
roles: Array<RoleSchema>;
|
||||
};
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { PlayerSchema } from './PlayerSchema';
|
||||
import type { RoleSchema } from './RoleSchema';
|
||||
export type PlayerRoleSchema = {
|
||||
player: PlayerSchema;
|
||||
role: RoleSchema;
|
||||
};
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
import type { TeamDiscordIntegrationSchema } from './TeamDiscordIntegrationSchema';
|
||||
import type { TeamLogsTfIntegrationSchema } from './TeamLogsTfIntegrationSchema';
|
||||
export type TeamIntegrationSchema = {
|
||||
discordIntegration?: TeamDiscordIntegrationSchema;
|
||||
logsTfIntegration?: TeamLogsTfIntegrationSchema;
|
||||
discordIntegration: (TeamDiscordIntegrationSchema | null);
|
||||
logsTfIntegration: (TeamLogsTfIntegrationSchema | null);
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type TeamLogsTfIntegrationSchema = {
|
||||
logsTfApiKey: string;
|
||||
logsTfApiKey: (string | null);
|
||||
minTeamMemberCount: number;
|
||||
};
|
||||
|
||||
|
|
|
@ -2,9 +2,6 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* An enumeration.
|
||||
*/
|
||||
export enum TeamRole {
|
||||
'_0' = 0,
|
||||
'_1' = 1,
|
||||
|
|
|
@ -8,6 +8,7 @@ import type { CreateTeamJson } from '../models/CreateTeamJson';
|
|||
import type { EditMemberRolesJson } from '../models/EditMemberRolesJson';
|
||||
import type { EventSchema } from '../models/EventSchema';
|
||||
import type { EventSchemaList } from '../models/EventSchemaList';
|
||||
import type { GetEventPlayersResponse } from '../models/GetEventPlayersResponse';
|
||||
import type { PlayerSchema } from '../models/PlayerSchema';
|
||||
import type { PutScheduleForm } from '../models/PutScheduleForm';
|
||||
import type { SetUsernameJson } from '../models/SetUsernameJson';
|
||||
|
@ -62,7 +63,7 @@ export class DefaultService {
|
|||
'team_id': teamId,
|
||||
},
|
||||
errors: {
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -73,7 +74,7 @@ export class DefaultService {
|
|||
* @returns EventSchema OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public postApiEventsTeamIdTeamId(
|
||||
public createEvent(
|
||||
teamId: number,
|
||||
requestBody?: CreateEventJson,
|
||||
): CancelablePromise<EventSchema> {
|
||||
|
@ -86,7 +87,7 @@ export class DefaultService {
|
|||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -123,7 +124,27 @@ export class DefaultService {
|
|||
'event_id': eventId,
|
||||
},
|
||||
errors: {
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* get_event_players <GET>
|
||||
* @param eventId
|
||||
* @returns GetEventPlayersResponse OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public getEventPlayers(
|
||||
eventId: number,
|
||||
): CancelablePromise<GetEventPlayersResponse> {
|
||||
return this.httpRequest.request({
|
||||
method: 'GET',
|
||||
url: '/api/events/{event_id}/players',
|
||||
path: {
|
||||
'event_id': eventId,
|
||||
},
|
||||
errors: {
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -144,6 +165,52 @@ export class DefaultService {
|
|||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* unattend_event <DELETE>
|
||||
* @param eventId
|
||||
* @param teamId
|
||||
* @returns void
|
||||
* @throws ApiError
|
||||
*/
|
||||
public deleteApiEventsEventIdTeamIdTeamIdAttendance(
|
||||
eventId: number,
|
||||
teamId: number,
|
||||
): CancelablePromise<void> {
|
||||
return this.httpRequest.request({
|
||||
method: 'DELETE',
|
||||
url: '/api/events/{event_id}/team/id/{team_id}/attendance',
|
||||
path: {
|
||||
'event_id': eventId,
|
||||
'team_id': teamId,
|
||||
},
|
||||
errors: {
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* attend_event <PUT>
|
||||
* @param eventId
|
||||
* @param teamId
|
||||
* @returns void
|
||||
* @throws ApiError
|
||||
*/
|
||||
public putApiEventsEventIdTeamIdTeamIdAttendance(
|
||||
eventId: number,
|
||||
teamId: number,
|
||||
): CancelablePromise<void> {
|
||||
return this.httpRequest.request({
|
||||
method: 'PUT',
|
||||
url: '/api/events/{event_id}/team/id/{team_id}/attendance',
|
||||
path: {
|
||||
'event_id': eventId,
|
||||
'team_id': teamId,
|
||||
},
|
||||
errors: {
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* logout <DELETE>
|
||||
* @returns void
|
||||
|
@ -188,7 +255,7 @@ export class DefaultService {
|
|||
url: '/api/login/get-user',
|
||||
errors: {
|
||||
401: `Unauthorized`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -214,7 +281,7 @@ export class DefaultService {
|
|||
'windowSizeDays': windowSizeDays,
|
||||
},
|
||||
errors: {
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -256,7 +323,7 @@ export class DefaultService {
|
|||
'windowSizeDays': windowSizeDays,
|
||||
},
|
||||
errors: {
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -279,7 +346,7 @@ export class DefaultService {
|
|||
'teamId': teamId,
|
||||
},
|
||||
errors: {
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -299,7 +366,7 @@ export class DefaultService {
|
|||
mediaType: 'application/json',
|
||||
errors: {
|
||||
403: `Forbidden`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -315,7 +382,7 @@ export class DefaultService {
|
|||
errors: {
|
||||
403: `Forbidden`,
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -337,7 +404,7 @@ export class DefaultService {
|
|||
errors: {
|
||||
403: `Forbidden`,
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -359,7 +426,7 @@ export class DefaultService {
|
|||
errors: {
|
||||
403: `Forbidden`,
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -383,7 +450,7 @@ export class DefaultService {
|
|||
},
|
||||
errors: {
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -412,7 +479,7 @@ export class DefaultService {
|
|||
errors: {
|
||||
403: `Forbidden`,
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -432,12 +499,12 @@ export class DefaultService {
|
|||
'team_id': teamId,
|
||||
},
|
||||
errors: {
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* update_integration <PUT>
|
||||
* update_integrations <PUT>
|
||||
* @param teamId
|
||||
* @param requestBody
|
||||
* @returns TeamIntegrationSchema OK
|
||||
|
@ -456,7 +523,7 @@ export class DefaultService {
|
|||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -477,7 +544,7 @@ export class DefaultService {
|
|||
},
|
||||
errors: {
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -498,7 +565,7 @@ export class DefaultService {
|
|||
},
|
||||
errors: {
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -522,7 +589,7 @@ export class DefaultService {
|
|||
},
|
||||
errors: {
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -551,7 +618,7 @@ export class DefaultService {
|
|||
errors: {
|
||||
403: `Forbidden`,
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -576,7 +643,7 @@ export class DefaultService {
|
|||
errors: {
|
||||
403: `Forbidden`,
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -598,7 +665,7 @@ export class DefaultService {
|
|||
errors: {
|
||||
403: `Forbidden`,
|
||||
404: `Not Found`,
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -617,7 +684,7 @@ export class DefaultService {
|
|||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Unprocessable Entity`,
|
||||
422: `Unprocessable Content`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -41,19 +41,19 @@ const props = defineProps<{
|
|||
<div>
|
||||
<h3>{{ event.name }}</h3>
|
||||
<div>
|
||||
<span v-if="event.description">{{ event.description }}</span>
|
||||
<em v-else class="subtext">No description provided.</em>
|
||||
<span>
|
||||
<i class="bi bi-clock-fill margin" />
|
||||
{{ formattedTime }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subdetails">
|
||||
<span>
|
||||
<i class="bi bi-clock-fill margin" />
|
||||
{{ formattedTime }}
|
||||
</span>
|
||||
<span class="class-info">
|
||||
<span v-if="event.description">{{ event.description }}</span>
|
||||
<em v-else class="subtext">No description provided.</em>
|
||||
<button class="class-info">
|
||||
<i class="tf2class tf2-PocketScout margin" />
|
||||
Pocket Scout
|
||||
</span>
|
||||
Accept as Pocket Scout
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -35,6 +35,11 @@ const router = createRouter({
|
|||
name: "roster-builder",
|
||||
component: RosterBuilderView
|
||||
},
|
||||
{
|
||||
path: "/schedule/roster/event/:eventId",
|
||||
name: "roster-builder-event",
|
||||
component: RosterBuilderView
|
||||
},
|
||||
{
|
||||
path: "/team/register",
|
||||
name: "team-registration",
|
||||
|
|
|
@ -2,10 +2,16 @@ import { type Player, type PlayerTeamRoleFlat } from "@/player";
|
|||
import { defineStore } from "pinia";
|
||||
import { computed, reactive, ref, type Reactive, type Ref } from "vue";
|
||||
import { useClientStore } from "./client";
|
||||
import { type EventSchema, type CreateEventJson, type PlayerRoleSchema } from "@/client";
|
||||
import { useTeamDetails } from "@/composables/team-details";
|
||||
import moment from "moment";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
export const useRosterStore = defineStore("roster", () => {
|
||||
const clientStore = useClientStore();
|
||||
const client = clientStore.client;
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
||||
const neededRoles: Reactive<Array<String>> = reactive([
|
||||
"PocketScout",
|
||||
|
@ -106,22 +112,94 @@ export const useRosterStore = defineStore("roster", () => {
|
|||
fetchAvailablePlayers.name,
|
||||
() => client.default.viewAvailableAtTime(startTime.toString(), teamId),
|
||||
(response) => {
|
||||
availablePlayers.value = response.players.flatMap((schema) => {
|
||||
return schema.roles.map((role) => ({
|
||||
steamId: schema.player.steamId,
|
||||
name: schema.player.username,
|
||||
role: role.role,
|
||||
isMain: role.isMain,
|
||||
availability: schema.availability,
|
||||
playtime: schema.playtime,
|
||||
}));
|
||||
});
|
||||
availablePlayers.value = response.players
|
||||
.flatMap((schema) => {
|
||||
return schema.roles.map((role) => ({
|
||||
steamId: schema.player.steamId,
|
||||
name: schema.player.username,
|
||||
role: role.role,
|
||||
isMain: role.isMain,
|
||||
availability: schema.availability,
|
||||
playtime: schema.playtime,
|
||||
}));
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function fetchPlayersFromEvent(eventId: number) {
|
||||
return clientStore.call(
|
||||
fetchPlayersFromEvent.name,
|
||||
() => client.default.getEventPlayers(eventId),
|
||||
(response) => {
|
||||
availablePlayers.value = response.players
|
||||
.flatMap((schema) => {
|
||||
return schema.roles.map((role) => ({
|
||||
steamId: schema.player.steamId,
|
||||
name: schema.player.username,
|
||||
role: role.role,
|
||||
isMain: role.isMain,
|
||||
availability: schema.hasConfirmed ? 2 : 1,
|
||||
playtime: schema.playtime,
|
||||
}));
|
||||
});
|
||||
|
||||
response.players
|
||||
.forEach((schema) => {
|
||||
if (schema.role) {
|
||||
selectedPlayers[schema.role.role] = {
|
||||
steamId: schema.player.steamId,
|
||||
name: schema.player.username,
|
||||
role: schema.role.role,
|
||||
isMain: schema.role.isMain,
|
||||
availability: schema.hasConfirmed ? 2 : 1,
|
||||
playtime: schema.playtime,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return response;
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const currentEvent = ref<EventSchema | undefined>(undefined);
|
||||
|
||||
const startTime = ref<number>();
|
||||
|
||||
function saveRoster(teamId: number) {
|
||||
if (startTime.value == undefined) {
|
||||
throw new Error("No start time set");
|
||||
}
|
||||
|
||||
if (!currentEvent.value) {
|
||||
const body: CreateEventJson = {
|
||||
name: "Test",
|
||||
description: "test description",
|
||||
startTime: startTime.value.toString(),
|
||||
playerRoles: Object.values(selectedPlayers).map((player) => ({
|
||||
player: {
|
||||
steamId: player.steamId,
|
||||
username: player.name,
|
||||
},
|
||||
role: {
|
||||
role: player.role,
|
||||
isMain: player.isMain,
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
clientStore.client.default.createEvent(teamId, body)
|
||||
.then(() => {
|
||||
|
||||
});
|
||||
} else {
|
||||
// TODO: update event
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
neededRoles,
|
||||
selectedPlayers,
|
||||
|
@ -136,5 +214,8 @@ export const useRosterStore = defineStore("roster", () => {
|
|||
mainRoles,
|
||||
alternateRoles,
|
||||
fetchAvailablePlayers,
|
||||
fetchPlayersFromEvent,
|
||||
startTime,
|
||||
saveRoster,
|
||||
}
|
||||
});
|
||||
|
|
|
@ -6,8 +6,10 @@ import { computed, reactive, onMounted } from "vue";
|
|||
import { useRosterStore } from "../stores/roster";
|
||||
import { useRoute } from "vue-router";
|
||||
import moment from "moment";
|
||||
import { useEventsStore } from "@/stores/events";
|
||||
|
||||
const rosterStore = useRosterStore();
|
||||
const eventsStore = useEventsStore();
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
|
@ -19,8 +21,21 @@ const hasAlternates = computed(() => {
|
|||
return rosterStore.alternateRoles.length > 0;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
rosterStore.fetchAvailablePlayers(route.params.startTime, route.params.teamId);
|
||||
const eventId = computed<number | undefined>(() => Number(route.params.eventId));
|
||||
|
||||
function saveRoster() {
|
||||
rosterStore.saveRoster(Number(route.params.teamId));
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (eventId.value) {
|
||||
const event = await eventsStore.fetchEvent(eventId.value);
|
||||
rosterStore.startTime = moment(event.startTime).unix();
|
||||
rosterStore.fetchPlayersFromEvent(eventId.value);
|
||||
} else {
|
||||
rosterStore.startTime = Number(route.params.startTime);
|
||||
rosterStore.fetchAvailablePlayers(rosterStore.startTime, Number(route.params.teamId));
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -29,14 +44,14 @@ onMounted(() => {
|
|||
<div class="top">
|
||||
<h1 class="roster-title">
|
||||
Roster for Snus Brotherhood
|
||||
<em class="aside date">
|
||||
<em class="aside date" v-if="rosterStore.startTime">
|
||||
@
|
||||
{{ moment.unix(route.params.startTime).format("L LT") }}
|
||||
{{ moment.unix(rosterStore.startTime).format("L LT") }}
|
||||
</em>
|
||||
</h1>
|
||||
<div class="button-group">
|
||||
<button>Cancel</button>
|
||||
<button class="accent">Save Roster</button>
|
||||
<button class="accent" @click="saveRoster">Save Roster</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="columns">
|
||||
|
|
|
@ -23,4 +23,4 @@ def connect_db_with_app():
|
|||
metadata = MetaData(naming_convention=convention)
|
||||
app = Flask(__name__)
|
||||
db = SQLAlchemy(model_class=BaseModel, metadata=metadata)
|
||||
migrate = Migrate(render_as_batch=True)
|
||||
migrate = Migrate(app, db, render_as_batch=True)
|
||||
|
|
|
@ -8,12 +8,16 @@
|
|||
|
||||
from datetime import datetime
|
||||
|
||||
from flask import Blueprint, abort
|
||||
from flask import Blueprint, abort, make_response
|
||||
from spectree import Response
|
||||
from models.player_event import PlayerEvent
|
||||
from sqlalchemy.sql import tuple_
|
||||
from models.player import Player
|
||||
from models.player_event import PlayerEvent, PlayerEventRolesSchema
|
||||
from models.player_team_availability import PlayerTeamAvailability
|
||||
from models.player_team_role import PlayerRoleSchema, PlayerTeamRole
|
||||
from models.team import Team
|
||||
from spec import BaseModel, spec
|
||||
from middleware import assert_team_authority, requires_authentication, requires_team_membership
|
||||
from middleware import assert_team_authority, assert_team_membership, requires_authentication, requires_team_membership
|
||||
from models.event import Event, EventSchema
|
||||
from models.player_team import PlayerTeam
|
||||
from app_db import db
|
||||
|
@ -65,17 +69,18 @@ class CreateEventJson(BaseModel):
|
|||
name: str
|
||||
description: str
|
||||
start_time: datetime
|
||||
player_ids: list[int]
|
||||
player_roles: list[PlayerRoleSchema]
|
||||
|
||||
@api_events.post("/team/id/<int:team_id>")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_200=EventSchema,
|
||||
)
|
||||
),
|
||||
operation_id="create_event",
|
||||
)
|
||||
@requires_authentication
|
||||
@requires_team_membership()
|
||||
def create_event(player_team: PlayerTeam, json: CreateEventJson, **_):
|
||||
def create_event(player_team: PlayerTeam, team_id: int, json: CreateEventJson, **_):
|
||||
event = Event()
|
||||
event.team_id = player_team.team_id
|
||||
event.name = json.name
|
||||
|
@ -86,27 +91,149 @@ def create_event(player_team: PlayerTeam, json: CreateEventJson, **_):
|
|||
db.session.flush()
|
||||
db.session.refresh(event)
|
||||
|
||||
players_teams = db.session.query(
|
||||
PlayerTeam
|
||||
tuples = map(lambda x: (x.player.steam_id, x.role.role), json.player_roles)
|
||||
|
||||
results = db.session.query(
|
||||
PlayerTeam, PlayerTeamRole.id, PlayerTeamAvailability.availability
|
||||
).join(
|
||||
Player
|
||||
PlayerTeamRole
|
||||
).outerjoin(
|
||||
PlayerTeamAvailability,
|
||||
(PlayerTeamAvailability.player_team_id == PlayerTeam.id) &
|
||||
(PlayerTeamAvailability.start_time <= event.start_time) &
|
||||
(PlayerTeamAvailability.end_time > event.start_time)
|
||||
).where(
|
||||
PlayerTeam.team_id == player_team.team_id
|
||||
PlayerTeam.team_id == team_id
|
||||
).where(
|
||||
PlayerTeam.player_id.in_(json.player_ids)
|
||||
# (player_id, role) in (...)
|
||||
tuple_(PlayerTeam.player_id, PlayerTeamRole.role).in_(tuples)
|
||||
).all()
|
||||
|
||||
for player_team in players_teams:
|
||||
player = player_team.player
|
||||
for player_team, role_id, availability in map(lambda x: x.tuple(), results):
|
||||
player_event = PlayerEvent()
|
||||
player_event.player_id = player.steam_id
|
||||
player_event.player_id = player_team.player_id
|
||||
player_event.event_id = event.id
|
||||
player_event.player_team_role_id = role_id
|
||||
|
||||
# autoconfirm if availability = 2
|
||||
player_event.has_confirmed = (availability == 2)
|
||||
|
||||
db.session.add(player_event)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
event.update_discord_message()
|
||||
|
||||
return EventSchema.from_model(event).dict(by_alias=True), 200
|
||||
|
||||
@api_events.put("/<int:event_id>/attendance")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_204=None,
|
||||
)
|
||||
)
|
||||
@requires_authentication
|
||||
@requires_team_membership()
|
||||
def attend_event(player_team: PlayerTeam, event_id: int, **_):
|
||||
player_event = db.session.query(
|
||||
PlayerEvent
|
||||
).where(
|
||||
PlayerEvent.event_id == event_id
|
||||
).where(
|
||||
PlayerEvent.player_id == player_team.player_id
|
||||
).join(
|
||||
Event
|
||||
).where(
|
||||
Event.team_id == player_team.team_id
|
||||
).one_or_none()
|
||||
|
||||
if not player_event:
|
||||
player_event = PlayerEvent()
|
||||
player_event.event_id = event_id
|
||||
player_event.player_id = player_team.player_id
|
||||
db.session.add(player_event)
|
||||
|
||||
player_event.has_confirmed = True
|
||||
|
||||
db.session.commit()
|
||||
|
||||
player_event.event.update_discord_message()
|
||||
|
||||
return make_response({ }, 204)
|
||||
|
||||
@api_events.delete("/<int:event_id>/attendance")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_204=None,
|
||||
)
|
||||
)
|
||||
@requires_authentication
|
||||
@requires_team_membership()
|
||||
def unattend_event(player_team: PlayerTeam, event_id: int, **_):
|
||||
result = db.session.query(
|
||||
PlayerEvent, Event
|
||||
).where(
|
||||
PlayerEvent.event_id == event_id
|
||||
).where(
|
||||
PlayerEvent.player_id == player_team.player_id
|
||||
).join(
|
||||
Event
|
||||
).where(
|
||||
Event.team_id == player_team.team_id
|
||||
).one_or_none()
|
||||
|
||||
if not result:
|
||||
abort(404)
|
||||
|
||||
player_event, event = result.tuple()
|
||||
|
||||
db.session.delete(player_event)
|
||||
db.session.commit()
|
||||
|
||||
event.update_discord_message()
|
||||
|
||||
return make_response({ }, 204)
|
||||
|
||||
class GetEventPlayersResponse(BaseModel):
|
||||
players: list[PlayerEventRolesSchema]
|
||||
|
||||
@api_events.get("/<int:event_id>/players")
|
||||
@spec.validate(
|
||||
resp=Response(
|
||||
HTTP_200=GetEventPlayersResponse,
|
||||
),
|
||||
operation_id="get_event_players",
|
||||
)
|
||||
@requires_authentication
|
||||
def get_event_players(player: Player, event_id: int, **_):
|
||||
event = db.session.query(Event).where(Event.id == event_id).one_or_none()
|
||||
if not event:
|
||||
abort(404)
|
||||
assert_team_membership(player, event.team)
|
||||
|
||||
players_events = db.session.query(
|
||||
PlayerEvent
|
||||
).join(
|
||||
Event,
|
||||
Event.id == PlayerEvent.event_id
|
||||
).join(
|
||||
PlayerTeam,
|
||||
PlayerTeam.team_id == Event.team_id & PlayerEvent.player_id == PlayerTeam.player_id
|
||||
).where(
|
||||
PlayerEvent.event_id == event_id
|
||||
).all()
|
||||
|
||||
player_event_roles = [
|
||||
PlayerEventRolesSchema.from_event_player_team(
|
||||
player_event, player_event.player_team
|
||||
)
|
||||
for player_event in players_events
|
||||
]
|
||||
|
||||
return GetEventPlayersResponse(
|
||||
players=player_event_roles
|
||||
).dict(by_alias=True), 200
|
||||
|
||||
@api_events.patch("/<int:event_id>/players")
|
||||
@requires_authentication
|
||||
@requires_team_membership()
|
||||
|
|
|
@ -6,6 +6,7 @@ from app_db import db
|
|||
from models.auth_session import AuthSession
|
||||
from models.player import Player
|
||||
from models.player_team import PlayerTeam
|
||||
from models.team import Team
|
||||
|
||||
|
||||
def requires_authentication(f):
|
||||
|
@ -72,6 +73,20 @@ def requires_team_membership(
|
|||
return decorator
|
||||
return wrapper
|
||||
|
||||
def assert_team_membership(player: Player, team: Team):
|
||||
player_team = db.session.query(
|
||||
PlayerTeam
|
||||
).where(
|
||||
PlayerTeam.player == player
|
||||
).where(
|
||||
PlayerTeam.team == team
|
||||
).one_or_none()
|
||||
|
||||
if not player_team:
|
||||
abort(404)
|
||||
|
||||
return player_team
|
||||
|
||||
def assert_team_authority(
|
||||
player_team: PlayerTeam,
|
||||
target_player_team: PlayerTeam | None = None,
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
"""Add event.discord_message_id
|
||||
|
||||
Revision ID: 286ee26b9e5d
|
||||
Revises: 392454b91293
|
||||
Create Date: 2024-11-25 21:00:08.444434
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '286ee26b9e5d'
|
||||
down_revision = '392454b91293'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('events', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('discord_message_id', sa.BigInteger(), nullable=True))
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('events', schema=None) as batch_op:
|
||||
batch_op.drop_column('discord_message_id')
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -3,9 +3,10 @@ from sqlalchemy.orm import mapped_column, relationship
|
|||
from sqlalchemy.orm.attributes import Mapped
|
||||
from sqlalchemy.orm.properties import ForeignKey
|
||||
from sqlalchemy.schema import UniqueConstraint
|
||||
from sqlalchemy.types import TIMESTAMP, Integer, String, Text
|
||||
from sqlalchemy.types import TIMESTAMP, BigInteger, Integer, String, Text
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy_utc import UtcDateTime
|
||||
from discord_webhook import DiscordWebhook
|
||||
import app_db
|
||||
import spec
|
||||
|
||||
|
@ -23,14 +24,81 @@ class Event(app_db.BaseModel):
|
|||
|
||||
description: Mapped[str] = mapped_column(Text, nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(TIMESTAMP, server_default=func.now())
|
||||
discord_message_id: Mapped[int | None] = mapped_column(BigInteger, nullable=True)
|
||||
|
||||
team: Mapped["Team"] = relationship("Team", back_populates="events")
|
||||
players: Mapped["PlayerEvent"] = relationship("PlayerEvent", back_populates="event")
|
||||
players: Mapped[list["PlayerEvent"]] = relationship("PlayerEvent", back_populates="event")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint("team_id", "name", "start_time"),
|
||||
)
|
||||
|
||||
def get_discord_content(self):
|
||||
start_timestamp = int(self.start_time.timestamp())
|
||||
players = list(self.players)
|
||||
# players with a role should be sorted first
|
||||
players.sort(key=lambda p: p.role is not None, reverse=True)
|
||||
players_info = []
|
||||
|
||||
for player in players:
|
||||
player_info = "- "
|
||||
|
||||
if player.role:
|
||||
player_info += f"**{player.role.role.name}:** "
|
||||
|
||||
player_info += f"{player.player.username}"
|
||||
|
||||
if player.has_confirmed:
|
||||
player_info += " ✅"
|
||||
else:
|
||||
player_info += " ⏳"
|
||||
|
||||
players_info.append(player_info)
|
||||
|
||||
return "\n".join([
|
||||
f"# {self.name}",
|
||||
"",
|
||||
self.description or "*No description.*",
|
||||
"",
|
||||
f"<t:{start_timestamp}:f>",
|
||||
"\n".join(players_info),
|
||||
"",
|
||||
"[Confirm availability here]" +
|
||||
f"(https://availabili.tf/team/id/{self.team.id}/events/{self.id})",
|
||||
])
|
||||
|
||||
def get_or_create_webhook(self):
|
||||
integration = app_db.db.session.query(
|
||||
TeamDiscordIntegration
|
||||
).where(
|
||||
TeamDiscordIntegration.team_id == self.team_id
|
||||
).first()
|
||||
|
||||
if not integration:
|
||||
return None
|
||||
|
||||
if self.discord_message_id:
|
||||
return DiscordWebhook(
|
||||
integration.webhook_url,
|
||||
id=str(self.discord_message_id),
|
||||
)
|
||||
else:
|
||||
return DiscordWebhook(integration.webhook_url)
|
||||
|
||||
def update_discord_message(self):
|
||||
webhook = self.get_or_create_webhook()
|
||||
if webhook:
|
||||
webhook.content = self.get_discord_content()
|
||||
if webhook.id:
|
||||
webhook.edit()
|
||||
else:
|
||||
webhook.execute()
|
||||
if webhook_id := webhook.id:
|
||||
self.discord_message_id = int(webhook_id)
|
||||
app_db.db.session.commit()
|
||||
else:
|
||||
raise Exception("Failed to create webhook")
|
||||
|
||||
class EventSchema(spec.BaseModel):
|
||||
id: int
|
||||
team_id: int
|
||||
|
@ -50,6 +118,17 @@ class EventSchema(spec.BaseModel):
|
|||
created_at=model.created_at,
|
||||
)
|
||||
|
||||
class EventPlayersSchema(spec.BaseModel):
|
||||
players: list["PlayerEventRolesSchema"]
|
||||
|
||||
@classmethod
|
||||
def from_model(cls, model: Event) -> "EventPlayersSchema":
|
||||
return cls(
|
||||
players=[PlayerEventRolesSchema.from_model(player) for player in model.players],
|
||||
roles=[RoleSchema.from_model(player.role.role) for player in model.players if player.role],
|
||||
)
|
||||
|
||||
|
||||
from models.team import Team
|
||||
from models.player_event import PlayerEvent
|
||||
from models.team_integration import TeamDiscordIntegration
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
from typing import Optional
|
||||
from sqlalchemy.orm import mapped_column, relationship
|
||||
from sqlalchemy.orm.attributes import Mapped
|
||||
from sqlalchemy.orm.properties import ForeignKey
|
||||
from sqlalchemy.types import Boolean
|
||||
import app_db
|
||||
import spec
|
||||
|
||||
|
||||
class PlayerEvent(app_db.BaseModel):
|
||||
|
@ -15,9 +17,34 @@ class PlayerEvent(app_db.BaseModel):
|
|||
|
||||
event: Mapped["Event"] = relationship("Event", back_populates="players")
|
||||
player: Mapped["Player"] = relationship("Player", back_populates="events")
|
||||
player_team: Mapped["PlayerTeam"] = relationship(
|
||||
"PlayerTeam",
|
||||
secondary="players",
|
||||
primaryjoin="PlayerEvent.player_id == Player.steam_id",
|
||||
secondaryjoin="PlayerTeam.player_id == Player.steam_id",
|
||||
viewonly=True,
|
||||
)
|
||||
role: Mapped["PlayerTeamRole"] = relationship("PlayerTeamRole")
|
||||
|
||||
class PlayerEventRolesSchema(spec.BaseModel):
|
||||
player: "PlayerSchema"
|
||||
role: Optional["RoleSchema"]
|
||||
roles: list["RoleSchema"]
|
||||
has_confirmed: bool
|
||||
playtime: int
|
||||
|
||||
@classmethod
|
||||
def from_event_player_team(cls, player_event: "PlayerEvent", player_team: "PlayerTeam"):
|
||||
return cls(
|
||||
player=PlayerSchema.from_model(player_event.player),
|
||||
role=RoleSchema.from_model(player_event.role) if player_event.role else None,
|
||||
roles=[RoleSchema.from_model(role) for role in player_team.player_roles],
|
||||
has_confirmed=player_event.has_confirmed,
|
||||
playtime=int(player_team.playtime.total_seconds()),
|
||||
)
|
||||
|
||||
|
||||
from models.event import Event
|
||||
from models.player import Player
|
||||
from models.player_team_role import PlayerTeamRole
|
||||
from models.player import Player, PlayerSchema
|
||||
from models.player_team_role import PlayerTeamRole, RoleSchema
|
||||
from models.player_team import PlayerTeam
|
||||
|
|
|
@ -53,7 +53,6 @@ class PlayerTeamRole(app_db.BaseModel):
|
|||
UniqueConstraint("player_team_id", "role"),
|
||||
)
|
||||
|
||||
|
||||
class RoleSchema(spec.BaseModel):
|
||||
role: str
|
||||
is_main: bool
|
||||
|
@ -62,5 +61,10 @@ class RoleSchema(spec.BaseModel):
|
|||
def from_model(cls, role: PlayerTeamRole):
|
||||
return cls(role=role.role.name, is_main=role.is_main)
|
||||
|
||||
class PlayerRoleSchema(spec.BaseModel):
|
||||
player: "PlayerSchema"
|
||||
role: RoleSchema
|
||||
|
||||
|
||||
from models.player_team import PlayerTeam
|
||||
from models.player import PlayerSchema
|
||||
|
|
|
@ -20,3 +20,5 @@ Flask-Migrate
|
|||
requests
|
||||
|
||||
pytz # timezone handling
|
||||
|
||||
discord-webhook # for sending messages to Discord webhooks
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import datetime
|
||||
from typing import cast
|
||||
from typing import Optional, cast
|
||||
from flask import Blueprint, abort, jsonify, make_response, request
|
||||
from spectree import Response
|
||||
from sqlalchemy import Row
|
||||
from sqlalchemy.orm import contains_eager, joinedload
|
||||
from sqlalchemy.sql import and_, select
|
||||
from app_db import db
|
||||
from models.player import Player, PlayerSchema
|
||||
from models.player_event import PlayerEvent, PlayerEventRolesSchema
|
||||
from models.player_team import PlayerTeam
|
||||
from models.player_team_availability import AvailabilitySchema, PlayerTeamAvailability, PlayerTeamAvailabilityRoleSchema
|
||||
from models.player_team_role import PlayerTeamRole, RoleSchema
|
||||
|
|
|
@ -397,22 +397,47 @@ def edit_member_roles(
|
|||
if not target_player:
|
||||
abort(401)
|
||||
|
||||
# TODO: change this to a MERGE statement
|
||||
|
||||
"""
|
||||
MERGE INTO players_teams_roles AS target
|
||||
USING (
|
||||
VALUES
|
||||
('PocketScout', 1),
|
||||
('PocketScout', 0),
|
||||
) AS source(role, is_main)
|
||||
ON (target.player_team_id = :player_team_id AND target.role = source.role)
|
||||
WHEN MATCHED THEN
|
||||
UPDATE SET
|
||||
target.role = source.role,
|
||||
target.is_main = source.is_main
|
||||
WHEN NOT MATCHED BY TARGET THEN
|
||||
INSERT (player_team_id, role, is_main)
|
||||
VALUES (:player_team_id, source.role, source.is_main)
|
||||
WHEN NOT MATCHED BY SOURCE THEN
|
||||
DELETE;
|
||||
"""
|
||||
|
||||
for role in target_player.player_roles:
|
||||
# delete role if not found in json
|
||||
f = filter(lambda x: x.role == role.role.name, json.roles)
|
||||
matched_role = next(f, None)
|
||||
|
||||
if not matched_role:
|
||||
if matched_role:
|
||||
# update
|
||||
role.is_main = matched_role.is_main
|
||||
else:
|
||||
db.session.delete(role)
|
||||
|
||||
for schema in json.roles:
|
||||
role = PlayerTeamRole()
|
||||
role.player_team = target_player
|
||||
role.role = PlayerTeamRole.Role[schema.role]
|
||||
role.is_main = schema.is_main
|
||||
db.session.merge(role)
|
||||
# insert if not found in target
|
||||
f = filter(lambda x: x.role.name == schema.role, target_player.player_roles)
|
||||
|
||||
if not next(f, None):
|
||||
role = PlayerTeamRole()
|
||||
role.player_team_id = target_player.id
|
||||
role.role = PlayerTeamRole.Role[schema.role]
|
||||
role.is_main = schema.is_main
|
||||
db.session.add(role)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
|
|
Loading…
Reference in New Issue