feat(frontend): Create CRUD User Interface for Units and Foods

This commit is contained in:
hay-kot 2021-08-22 15:23:45 -08:00
parent 122d35ec09
commit a1aad078da
13 changed files with 517 additions and 42 deletions

View file

@ -10,39 +10,16 @@ export interface CrudAPIInterface {
// Methods
}
export const crudMixins = <T>(
requests: ApiRequestInstance,
baseRoute: string,
itemRoute: (itemId: string) => string
) => {
async function getAll(start = 0, limit = 9999) {
return await requests.get<T[]>(baseRoute, {
params: { start, limit },
});
}
export interface CrudAPIMethodsInterface {
// CRUD Methods
getAll(): any
createOne(): any
getOne(): any
updateOne(): any
patchOne(): any
deleteOne(): any
}
async function createOne(payload: T) {
return await requests.post<T>(baseRoute, payload);
}
async function getOne(itemId: string) {
return await requests.get<T>(itemRoute(itemId));
}
async function updateOne(itemId: string, payload: T) {
return await requests.put<T>(itemRoute(itemId), payload);
}
async function patchOne(itemId: string, payload: T) {
return await requests.patch(itemRoute(itemId), payload);
}
async function deleteOne(itemId: string) {
return await requests.delete<T>(itemRoute(itemId));
}
return { getAll, getOne, updateOne, patchOne, deleteOne, createOne };
};
export abstract class BaseAPI {
requests: ApiRequestInstance;
@ -66,11 +43,11 @@ export abstract class BaseCRUDAPI<T, U> extends BaseAPI implements CrudAPIInterf
return await this.requests.post<T>(this.baseRoute, payload);
}
async getOne(itemId: string) {
async getOne(itemId: string | number) {
return await this.requests.get<T>(this.itemRoute(itemId));
}
async updateOne(itemId: string, payload: T) {
async updateOne(itemId: string | number, payload: T) {
return await this.requests.put<T>(this.itemRoute(itemId), payload);
}

View file

@ -1,4 +1,3 @@
import { requests } from "../requests";
import { BaseCRUDAPI } from "./_base";
export type EventCategory = "general" | "recipe" | "backup" | "scheduled" | "migration" | "group" | "user";
@ -36,7 +35,7 @@ export class NotificationsAPI extends BaseCRUDAPI<EventNotification, CreateEvent
itemRoute = routes.aboutEventsNotificationsId;
/** Returns the Group Data for the Current User
*/
async testNotification(id: number) {
return await requests.post(routes.aboutEventsNotificationsTest, { id });
async testNotification(id: number | null = null, testUrl: string | null = null) {
return await this.requests.post(routes.aboutEventsNotificationsTest, { id, testUrl });
}
}

View file

@ -0,0 +1,22 @@
import { BaseCRUDAPI } from "./_base";
const prefix = "/api";
export interface CreateFood {
name: string;
description: string;
}
export interface Food extends CreateFood {
id: number;
}
const routes = {
food: `${prefix}/foods`,
foodsFood: (tag: string) => `${prefix}/foods/${tag}`,
};
export class FoodAPI extends BaseCRUDAPI<Food, CreateFood> {
baseRoute: string = routes.food;
itemRoute = routes.foodsFood;
}

View file

@ -0,0 +1,23 @@
import { BaseCRUDAPI } from "./_base";
const prefix = "/api";
export interface CreateUnit {
name: string;
abbreviation: string;
description: string;
}
export interface Unit extends CreateUnit {
id: number;
}
const routes = {
unit: `${prefix}/units`,
unitsUnit: (tag: string) => `${prefix}/units/${tag}`,
};
export class UnitAPI extends BaseCRUDAPI<Unit, CreateUnit> {
baseRoute: string = routes.unit;
itemRoute = routes.unitsUnit;
}

View file

@ -9,6 +9,8 @@ import { CategoriesAPI } from "./class-interfaces/categories";
import { TagsAPI } from "./class-interfaces/tags";
import { UtilsAPI } from "./class-interfaces/utils";
import { NotificationsAPI } from "./class-interfaces/event-notifications";
import { FoodAPI } from "./class-interfaces/recipe-foods";
import { UnitAPI } from "./class-interfaces/recipe-units";
import { ApiRequestInstance } from "~/types/api";
class Api {
@ -23,6 +25,8 @@ class Api {
public tags: TagsAPI;
public utils: UtilsAPI;
public notifications: NotificationsAPI;
public foods: FoodAPI;
public units: UnitAPI;
// Utils
public upload: UploadFile;
@ -36,6 +40,8 @@ class Api {
this.recipes = new RecipeAPI(requests);
this.categories = new CategoriesAPI(requests);
this.tags = new TagsAPI(requests);
this.units = new UnitAPI(requests);
this.foods = new FoodAPI(requests);
// Users
this.users = new UserApi(requests);

View file

@ -62,12 +62,14 @@ export const useNotifications = function () {
}
}
async function testById() {
// TODO: Test by ID
async function testById(id: number) {
const {data} = await api.notifications.testNotification(id, null)
console.log(data)
}
async function testByUrl() {
// TODO: Test by URL
async function testByUrl(testUrl: string) {
const {data} = await api.notifications.testNotification(null, testUrl)
console.log(data)
}
const notifications = getNotifications();

View file

@ -0,0 +1,91 @@
import { useAsync, ref, reactive } from "@nuxtjs/composition-api";
import { useAsyncKey } from "./use-utils";
import { useApiSingleton } from "~/composables/use-api";
import { Food } from "~/api/class-interfaces/recipe-foods";
export const useFoods = function () {
const api = useApiSingleton();
const loading = ref(false);
const deleteTargetId = ref(0);
const validForm = ref(true);
const workingFoodData = reactive({
id: 0,
name: "",
description: "",
});
const actions = {
getAll() {
loading.value = true;
const units = useAsync(async () => {
const { data } = await api.foods.getAll();
return data;
}, useAsyncKey());
loading.value = false
return units;
},
async refreshAll() {
loading.value = true;
const { data } = await api.foods.getAll();
if (data) {
foods.value = data;
}
loading.value = false;
},
async createOne(domForm: VForm | null = null) {
if (domForm && !domForm.validate()) {
validForm.value = false;
return;
}
loading.value = true;
const { data } = await api.foods.createOne(workingFoodData);
if (data && foods.value) {
foods.value.push(data);
} else {
this.refreshAll();
}
domForm?.reset();
validForm.value = true;
this.resetWorking();
loading.value = false;
},
async updateOne() {
if (!workingFoodData.id) {
return;
}
loading.value = true;
const { data } = await api.foods.updateOne(workingFoodData.id, workingFoodData);
if (data && foods.value) {
this.refreshAll();
}
loading.value = false;
},
async deleteOne(id: string | number) {
loading.value = true;
const { data } = await api.foods.deleteOne(id);
if (data && foods.value) {
this.refreshAll();
}
},
resetWorking() {
workingFoodData.id = 0;
workingFoodData.name = "";
workingFoodData.description = "";
},
setWorking(item: Food) {
workingFoodData.id = item.id;
workingFoodData.name = item.name;
workingFoodData.description = item.description;
},
};
const foods = actions.getAll();
return { foods, workingFoodData, deleteTargetId, actions, validForm };
};

View file

@ -0,0 +1,94 @@
import { useAsync, ref, reactive } from "@nuxtjs/composition-api";
import { useAsyncKey } from "./use-utils";
import { useApiSingleton } from "~/composables/use-api";
import { Unit } from "~/api/class-interfaces/recipe-units";
export const useUnits = function () {
const api = useApiSingleton();
const loading = ref(false);
const deleteTargetId = ref(0);
const validForm = ref(true);
const workingUnitData = reactive({
id: 0,
name: "",
abbreviation: "",
description: "",
});
const actions = {
getAll() {
loading.value = true;
const units = useAsync(async () => {
const { data } = await api.units.getAll();
return data;
}, useAsyncKey());
loading.value = false
return units;
},
async refreshAll() {
loading.value = true;
const { data } = await api.units.getAll();
if (data) {
units.value = data;
}
loading.value = false;
},
async createOne(domForm: VForm | null = null) {
if (domForm && !domForm.validate()) {
validForm.value = false;
return;
}
loading.value = true;
const { data } = await api.units.createOne(workingUnitData);
if (data && units.value) {
units.value.push(data);
} else {
this.refreshAll();
}
domForm?.reset();
validForm.value = true;
this.resetWorking();
loading.value = false;
},
async updateOne() {
if (!workingUnitData.id) {
return;
}
loading.value = true;
const { data } = await api.units.updateOne(workingUnitData.id, workingUnitData);
if (data && units.value) {
this.refreshAll();
}
loading.value = false;
},
async deleteOne(id: string | number) {
loading.value = true;
const { data } = await api.units.deleteOne(id);
if (data && units.value) {
this.refreshAll();
}
},
resetWorking() {
workingUnitData.id = 0;
workingUnitData.name = "";
workingUnitData.abbreviation = "";
workingUnitData.description = "";
},
setWorking(item: Unit) {
workingUnitData.id = item.id;
workingUnitData.name = item.name;
workingUnitData.abbreviation = item.abbreviation;
workingUnitData.description = item.description;
},
};
const units = actions.getAll();
return { units, workingUnitData, deleteTargetId, actions, validForm };
};

View file

@ -83,6 +83,16 @@ export default defineComponent({
to: "/admin/toolbox/notifications",
title: this.$t("events.notification"),
},
{
icon: this.$globals.icons.foods,
to: "/admin/toolbox/foods",
title: "Manage Foods",
},
{
icon: this.$globals.icons.units,
to: "/admin/toolbox/units",
title: "Manage Units",
},
{
icon: this.$globals.icons.tags,
to: "/admin/toolbox/categories",

View file

@ -0,0 +1,120 @@
<template>
<v-container fluid>
<BaseCardSectionTitle title="Manage Units"> </BaseCardSectionTitle>
<v-toolbar flat>
<BaseDialog
ref="domFoodDialog"
:title="dialog.title"
:icon="$globals.icons.units"
:submit-text="dialog.text"
:keep-open="!validForm"
@submit="create ? actions.createOne(domCreateFoodForm) : actions.updateOne()"
>
<v-card-text>
<v-form ref="domCreateFoodForm">
<v-text-field v-model="workingFoodData.name" label="Name" :rules="[validators.required]"></v-text-field>
<v-text-field v-model="workingFoodData.description" label="Description"></v-text-field>
</v-form>
</v-card-text>
</BaseDialog>
<BaseButton
class="mr-1"
@click="
create = true;
actions.resetWorking();
domFoodDialog.open();
"
></BaseButton>
<BaseButton secondary @click="filter = !filter"> Filter </BaseButton>
</v-toolbar>
<v-expand-transition>
<div v-show="filter">
<v-text-field v-model="search" style="max-width: 500px" label="Filter" class="ml-4"> </v-text-field>
</div>
</v-expand-transition>
<v-data-table :headers="headers" :items="foods || []" item-key="id" class="elevation-0" :search="search">
<template #item.actions="{ item }">
<div class="d-flex justify-end">
<BaseButton
edit
small
class="mr-2"
@click="
create = false;
actions.setWorking(item);
domFoodDialog.open();
"
></BaseButton>
<BaseDialog :title="$t('general.confirm')" color="error" @confirm="actions.deleteOne(item.id)">
<template #activator="{ open }">
<BaseButton delete small @click="open"></BaseButton>
</template>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
</v-card-text>
</BaseDialog>
</div>
</template>
</v-data-table>
<v-divider></v-divider>
</v-container>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, ref, computed } from "@nuxtjs/composition-api";
import { useFoods } from "~/composables/use-recipe-foods";
import { validators } from "~/composables/use-validators";
export default defineComponent({
layout: "admin",
setup() {
const { foods, actions, workingFoodData, validForm } = useFoods();
const domCreateFoodForm = ref(null);
const domFoodDialog = ref(null);
const dialog = computed(() => {
if (state.create) {
return {
title: "Create Food",
text: "Create",
};
} else {
return {
title: "Edit Food",
text: "Update",
};
}
});
const state = reactive({
headers: [
{ text: "Id", value: "id" },
{ text: "Name", value: "name" },
{ text: "Description", value: "description" },
{ text: "", value: "actions", sortable: false },
],
filter: false,
create: true,
search: "",
});
return {
...toRefs(state),
actions,
dialog,
domCreateFoodForm,
domFoodDialog,
foods,
validators,
validForm,
workingFoodData,
};
},
});
</script>
<style scoped>
</style>

View file

@ -47,7 +47,12 @@
:label="$t('events.apprise-url')"
></v-text-field>
<BaseButton class="d-flex ml-auto" small color="info" @click="testByUrl(newNotification.notificationUrl)">
<BaseButton
class="d-flex ml-auto"
small
color="info"
@click="testByUrl(createNotificationData.notificationUrl)"
>
<template #icon> {{ $globals.icons.testTube }}</template>
{{ $t("general.test") }}
</BaseButton>

View file

@ -0,0 +1,122 @@
<template>
<v-container fluid>
<BaseCardSectionTitle title="Manage Units"> </BaseCardSectionTitle>
<v-toolbar flat>
<BaseDialog
ref="domUnitDialog"
:title="dialog.title"
:icon="$globals.icons.units"
:submit-text="dialog.text"
:keep-open="!validForm"
@submit="create ? actions.createOne(domCreateUnitForm) : actions.updateOne()"
>
<v-card-text>
<v-form ref="domCreateUnitForm">
<v-text-field v-model="workingUnitData.name" label="Name" :rules="[validators.required]"></v-text-field>
<v-text-field v-model="workingUnitData.abbreviation" label="Abbreviation"></v-text-field>
<v-text-field v-model="workingUnitData.description" label="Description"></v-text-field>
</v-form>
</v-card-text>
</BaseDialog>
<BaseButton
class="mr-1"
@click="
create = true;
actions.resetWorking();
domUnitDialog.open();
"
></BaseButton>
<BaseButton secondary @click="filter = !filter"> Filter </BaseButton>
</v-toolbar>
<v-expand-transition>
<div v-show="filter">
<v-text-field v-model="search" style="max-width: 500px" label="Filter" class="ml-4"> </v-text-field>
</div>
</v-expand-transition>
<v-data-table :headers="headers" :items="units || []" item-key="id" class="elevation-0" :search="search">
<template #item.actions="{ item }">
<div class="d-flex justify-end">
<BaseButton
edit
small
class="mr-2"
@click="
create = false;
actions.setWorking(item);
domUnitDialog.open();
"
></BaseButton>
<BaseDialog :title="$t('general.confirm')" color="error" @confirm="actions.deleteOne(item.id)">
<template #activator="{ open }">
<BaseButton delete small @click="open"></BaseButton>
</template>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
</v-card-text>
</BaseDialog>
</div>
</template>
</v-data-table>
<v-divider></v-divider>
</v-container>
</template>
<script lang="ts">
import { defineComponent, reactive, toRefs, ref, computed } from "@nuxtjs/composition-api";
import { useUnits } from "~/composables/use-recipe-units";
import { validators } from "~/composables/use-validators";
export default defineComponent({
layout: "admin",
setup() {
const { units, actions, workingUnitData, validForm } = useUnits();
const domCreateUnitForm = ref(null);
const domUnitDialog = ref(null);
const dialog = computed(() => {
if (state.create) {
return {
title: "Create Unit",
text: "Create",
};
} else {
return {
title: "Edit Unit",
text: "Update",
};
}
});
const state = reactive({
headers: [
{ text: "Id", value: "id" },
{ text: "Name", value: "name" },
{ text: "Abbreviation", value: "abbreviation" },
{ text: "Description", value: "description" },
{ text: "", value: "actions", sortable: false },
],
filter: false,
create: true,
search: "",
});
return {
...toRefs(state),
actions,
dialog,
domCreateUnitForm,
domUnitDialog,
units,
validators,
validForm,
workingUnitData,
};
},
});
</script>
<style scoped>
</style>

View file

@ -92,6 +92,8 @@ import {
mdiMinus,
mdiWindowClose,
mdiFolderZipOutline,
mdiFoodApple,
mdiBeakerOutline,
} from "@mdi/js";
const icons = {
@ -99,6 +101,8 @@ const icons = {
primary: mdiSilverwareVariant,
// General
foods: mdiFoodApple,
units: mdiBeakerOutline,
alert: mdiAlert,
alertCircle: mdiAlertCircle,
api: mdiApi,