Skip to content

Commit

Permalink
✨ 129 feature admin panel (#130)
Browse files Browse the repository at this point in the history
* save

* save

* introduced apiFactory for DefaultApi Object

* replaced all with new apiFactory

* added api requests

* removed not needed dialog

* 🚚 moved api logic to main app and composable

* created admin board and authorities

* added few config modification options

* ✨ categories editable in any way

* 💄 disabled deletion button for standard category

* 💄 styling changes to admin page

* 💄 changed icon

* replaced link-text with icon

* outsourced loading of settings

* added ability to write single settings to store

* added tile for messageOfTheDay

* added first layout for generalSettings

* 🍻 added components for changing configs

* added isDirty flag upon single changes to the settings

* set transactional

* spelling mistake

* added component to upload files to config

* added converter for file and swbFile. format this

* added all settings to the general settings overview

* added api to update settings

* outsourced downloading of swbFiles and adapted logic of agb component

* 🚚 outsourced defaultQuery and removed old static query

* 🐛 left over from moving the api call

* 💄 exchanged the additional information

* 🚧 replaced the static files with api call and dynamic settings

* ♻️ moved constant and replaced selection logic with id only

* 🍺 adjusted composable to identify the filetype

* adjusted file input

* 🚨

---------

Co-authored-by: jannik.lange <[email protected]>
Co-authored-by: langehm <[email protected]>
  • Loading branch information
3 people authored Jan 31, 2025
1 parent 9ca577e commit b7fca01
Show file tree
Hide file tree
Showing 41 changed files with 1,166 additions and 347 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

import java.io.IOException;
Expand All @@ -29,6 +30,7 @@
import java.util.stream.Collectors;

@Component
@Profile("local")
public class BootstrapSwbrett implements CommandLineRunner {

private static final Logger LOG = LoggerFactory.getLogger(BootstrapSwbrett.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;
Expand All @@ -20,20 +21,22 @@ public class SettingService {
@Autowired
private SettingMapper mapper;

@Transactional
public List<SettingTO> getAllSettings() {
final List<Setting> all = repository.findAll();
return all.stream().map(mapper::toSettingTO).collect(Collectors.toList());
}

@Transactional
public SettingTO getSetting(final SettingName name) {
final Setting setting = repository.findBySettingName(name);
return mapper.toSettingTO(setting);
}

public SettingTO createSetting(final SettingTO settingTO) {
public void createSetting(final SettingTO settingTO) {
final Setting setting = mapper.toSetting(settingTO);
final Setting savedSetting = repository.save(setting);
return mapper.toSettingTO(savedSetting);
mapper.toSettingTO(savedSetting);
}

@PreAuthorize("hasAuthority(T(de.muenchen.anzeigenportal.security.AuthoritiesEnum).REFARCH_BACKEND_WRITE_THEENTITY.name())")
Expand Down
108 changes: 104 additions & 4 deletions anzeigen-frontend/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<router-link
:to="{
name: ROUTES_BOARD,
query: DEFAULT_BOARD_QUERIES,
query: defaultQuery,
}"
>
<v-toolbar-title class="font-weight-bold">
Expand All @@ -34,8 +34,19 @@
>
<search-ad />
</v-col>
<v-col cols="1">
<v-spacer />
<v-col cols="2">
<router-link
v-if="userStore.isAdmin"
:to="{ name: ROUTES_ADMIN }"
>
<v-toolbar-title class="font-weight-bold d-flex justify-end">
<v-icon
icon="mdi-cog"
color="white"
/>
</v-toolbar-title>
</router-link>
<v-spacer v-else />
</v-col>
</v-row>
</v-col>
Expand All @@ -60,15 +71,104 @@

<script setup lang="ts">
import { useTitle } from "@vueuse/core";
import { computed, onMounted } from "vue";
import { Levels } from "@/api/error.ts";
import SearchAd from "@/components/Filter/SearchAd.vue";
import TheSnackbarQueue from "@/components/TheSnackbarQueue.vue";
import {
useCreateUser,
useFindUser,
useUserInfo,
} from "@/composables/api/useUserApi.ts";
import { useUpdateCategories } from "@/composables/updateCategories.ts";
import { useApi } from "@/composables/useApi";
import { DEFAULT_BOARD_QUERIES, ROUTES_BOARD } from "@/Constants";
import { useDefaultQuery } from "@/composables/useDefaultQuery.ts";
import { useSnackbar } from "@/composables/useSnackbar.ts";
import { useUpdateSettings } from "@/composables/useUpdateSettings.ts";
import { API_ERROR_MSG, ROUTES_ADMIN, ROUTES_BOARD } from "@/Constants";
import { useCategoriesStore } from "@/stores/adcategory.ts";
import { useSettingStore } from "@/stores/settings.ts";
import { useUserStore } from "@/stores/user.ts";
useApi();
useTitle("Anzeigen Portal");
const defaultQuery = useDefaultQuery();
const updateCategories = useUpdateCategories();
const updateSettings = useUpdateSettings();
const settingStore = useSettingStore();
const userStore = useUserStore();
const snackbar = useSnackbar();
const categoriesStore = useCategoriesStore();
const {
call: userInfoCall,
data: userInfoData,
error: userInfoError,
} = useUserInfo();
const { call: findUserCall, data: findUserData } = useFindUser();
const { call: createUserCall, data: createUserData } = useCreateUser();
const currentUser = computed(() => findUserData.value || createUserData.value);
/**
* Initialize the stores
*/
onMounted(async () => {
if (!settingStore.isLoaded) {
await updateSettings();
}
if (!userStore.userID) {
await loadUser();
}
if (categoriesStore.isEmpty) {
await updateCategories();
}
});
/**
* Loads current user. Therefore, requests all parameters from the sso endpoint and matches those with the backend.
* If no user exists a new one will be created.
*/
const loadUser = async () => {
// userinfo call
await userInfoCall();
if (userInfoError.value) {
snackbar.sendMessage({
level: Levels.ERROR,
message: API_ERROR_MSG,
});
return;
}
// save into store
userStore.setUser(JSON.parse(JSON.stringify(userInfoData.value)));
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await findUserCall({ body: userStore.lhmObjectId! });
if (findUserData.value?.id === undefined) {
// no user was found - therefore create a new one
await createUserCall({
swbUserTO: {
displayName: userStore.getUser?.displayName,
lhmObjectId: userStore.lhmObjectId,
},
});
}
snackbar.sendMessage({
level: findUserData.value?.id ? Levels.INFO : Levels.SUCCESS,
message: `Willkommen ${findUserData.value?.id ? "" : "zurück"} ${currentUser.value?.displayName}.`,
});
userStore.setUserId(currentUser.value?.id || -1);
};
</script>

<style>
Expand Down
19 changes: 7 additions & 12 deletions anzeigen-frontend/src/Constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { AdTO, SwbUserTO } from "@/api/swbrett";
import type { InjectionKey, Ref } from "vue";

export const ROUTES_BOARD = "board";
export const ROUTES_MYBOARD = "myboard";
export const ROUTES_AD = "ad";
export const ROUTES_ADMIN = "admin";
export const ROUTES_GETSTARTED = "getstarted";

export const AD2IMAGE_URL = import.meta.env.VITE_AD2IMAGE_URL;
Expand Down Expand Up @@ -34,25 +34,20 @@ export const FILE_SIZE_TO_BIG = (size: string) =>
export const TOO_MANY_FILES = (amount: string) =>
"Die Anzahl der Dateien ist überschritten. Maximal " + amount + " Stück.";

/**
* Injection Keys
*/
export const IK_IS_MYBOARD: InjectionKey<Readonly<Ref<boolean>>> = Symbol(
"injection-key-my-board"
);

/**
* Other constants
*/
export const ADMIN_AUTHORITIES = [
"REFARCH_BACKEND_READ_THEENTITY",
"REFARCH_BACKEND_WRITE_THEENTITY",
"REFARCH_BACKEND_DELETE_THEENTITY",
];
export const NO_CATEGORY = { id: -1, name: "Alle", standard: true };
export const ALLOWED_FILE_TYPES =
"image/png, image/jpeg, image/jpg, application/pdf";
export const ALLOWED_IMAGE_TYPES = "image/png, image/jpeg, image/jpg";
export const AD_MAX_TITLE_LENGTH = 40;
export const DATE_DISPLAY_FORMAT = "DD.MM.YYYY"; // use this in conjunction with useDateFormat
export const DEFAULT_BOARD_QUERIES = {
sortBy: "title",
order: "asc",
};
export const PREVIEW_IMAGE_FILE_URI_PREFIX = "data:image/jpeg;base64,";
export const QUERY_NAME_ORDER = "order";
export const QUERY_NAME_SORTBY = "sortBy";
Expand Down
2 changes: 1 addition & 1 deletion anzeigen-frontend/src/components/Ad/AdCategoryChip.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<template>
<v-chip
variant="tonal"
prepend-icon="mdi-check-all"
prepend-icon="mdi-card-multiple"
color="accent"
:text="category.name"
/>
Expand Down
7 changes: 5 additions & 2 deletions anzeigen-frontend/src/components/Ad/Details/AdNotFound.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<router-link
:to="{
name: ROUTES_BOARD,
query: DEFAULT_BOARD_QUERIES,
query: defaultQuery,
}"
>
Klicken Sie hier um zur Übersicht zurückzukehren.
Expand All @@ -39,7 +39,10 @@
<script setup lang="ts">
import AdDisplayCard from "@/components/common/AdDisplayCard.vue";
import AdDisplaySheet from "@/components/common/AdDisplaySheet.vue";
import { DEFAULT_BOARD_QUERIES, ROUTES_BOARD } from "@/Constants";
import { useDefaultQuery } from "@/composables/useDefaultQuery.ts";
import { ROUTES_BOARD } from "@/Constants";
const defaultQuery = useDefaultQuery();
</script>

<style scoped></style>
67 changes: 22 additions & 45 deletions anzeigen-frontend/src/components/Ad/Details/AdOverview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,28 +90,6 @@
</v-col>
</v-row>
<v-row>
<v-col
v-if="adDetails.adFiles?.length !== 0"
cols="12"
lg="6"
>
<ad-display-card
:loading="getFileLoading"
:disabled="getFileLoading"
>
<template #subtitle>Weitere Informationen</template>
<template #text>
<icon-text
v-for="i in adDetails.adFiles"
:key="i.id"
class="mb-2 cursor-pointer"
:label="i.name!"
icon="link-variant"
@click="downloadFile(i.id!)"
/>
</template>
</ad-display-card>
</v-col>
<v-col
cols="12"
lg="6"
Expand Down Expand Up @@ -142,6 +120,25 @@
</template>
</ad-display-card>
</v-col>
<v-col
v-if="adDetails.adFiles?.length !== 0"
cols="12"
lg="6"
>
<ad-display-card>
<template #subtitle>Weitere Informationen</template>
<template #text>
<icon-text
v-for="i in adDetails.adFiles"
:key="i.id"
class="mb-2 cursor-pointer"
:label="i.name!"
icon="link-variant"
@click="downloadFile(i.id!)"
/>
</template>
</ad-display-card>
</v-col>
</v-row>
</v-container>
</template>
Expand All @@ -157,18 +154,18 @@ import AdImageDisplay from "@/components/Ad/Details/AdImageDisplay.vue";
import AdDisplayCard from "@/components/common/AdDisplayCard.vue";
import AdDisplaySheet from "@/components/common/AdDisplaySheet.vue";
import IconText from "@/components/common/IconText.vue";
import { useGetFile } from "@/composables/api/useFilesApi";
import { useDownloadFile } from "@/composables/useDownloadFile.ts";
import { DATE_DISPLAY_FORMAT } from "@/Constants";
import router from "@/plugins/router";
const downloadFile = useDownloadFile();
const { adDetails } = defineProps<{
adDetails: Readonly<AdTO>;
}>();
const currentLink = computed(() => window.location.href);
const { call: getFile, data: fileData, loading: getFileLoading } = useGetFile();
/**
* Computes the ad type, returning "Suche" for SEEK and "Biete" for other ad types.
*/
Expand All @@ -188,26 +185,6 @@ const routeToUser = (id: number) => {
},
});
};
/**
* Downloads a file based on the provided ID.
* Retrieves the file, creates a Blob, and triggers a download.
* @param id - The ID of the file to download.
*/
const downloadFile = async (id: number) => {
await getFile({ id: id });
if (fileData.value && fileData.value.fileBase64 && fileData.value.name) {
const blob = new Blob([fileData.value?.fileBase64]);
const fileURL = URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = fileURL;
downloadLink.download = fileData.value.name;
document.body.appendChild(downloadLink);
downloadLink.click();
}
};
</script>

<style scoped>
Expand Down
Loading

0 comments on commit b7fca01

Please sign in to comment.