Skip to content

Commit

Permalink
feat(admin): add unit management
Browse files Browse the repository at this point in the history
  • Loading branch information
daviddomkar committed Apr 29, 2024
1 parent 6f83ee3 commit e714790
Show file tree
Hide file tree
Showing 11 changed files with 320 additions and 49 deletions.
50 changes: 24 additions & 26 deletions components/UnitForm.vue
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
<script setup lang="ts">
import { UnitType } from "@prisma/client";
import type { Input, Output } from "valibot";
import type { SubmissionHandler } from "vee-validate";
const props = defineProps<{
initialValues?: Input<typeof CategoryFormSchema>;
onSubmit?: SubmissionHandler<Output<typeof CategoryFormSchema>>;
initialValues?: Input<typeof UnitFormSchema>;
onSubmit?: SubmissionHandler<Output<typeof UnitFormSchema>>;
}>();
const emit = defineEmits<{
(e: "cancel"): void;
}>();
const { handleSubmit, handleReset, isSubmitting, setValues } = useForm({
validationSchema: toTypedSchema(CategoryFormSchema),
validationSchema: toTypedSchema(UnitFormSchema),
initialValues: props.initialValues,
});
const submit = handleSubmit(async (values, opts) => {
await props.onSubmit?.(values, opts as any);
});
const categoryIconNames = [
"breakfast",
"lunch",
"dinner",
"snacks",
"soups",
"sauces",
"recipes",
"ingredients",
];
onMounted(() =>
setValues(
{
Expand All @@ -44,20 +34,28 @@ onMounted(() =>
<template>
<form class="w-full flex flex-col" @reset="handleReset" @submit="submit">
<TextField label="Title" name="title" />
<TextField label="Slug" name="slug" />
<SelectField
label="Icon"
name="icon"
:options="
categoryIconNames.map((name) => {
return {
key: name,
title: name,
value: name,
};
})
"
label="Type"
name="type"
:options="[
{
key: 'quantity',
title: 'Quantity',
value: UnitType.QUANTITY,
},
{
key: 'volume',
title: 'Volume',
value: UnitType.VOLUME,
},
{
key: 'weight',
title: 'Weight',
value: UnitType.WEIGHT,
},
]"
/>
<TextField label="Abbreviation" name="abbreviation" />
<div class="flex flex-row-reverse gap-2">
<BaseButton expanded :loading="isSubmitting" type="submit">
{{ props.initialValues?.id ? "Edit" : "Create" }}
Expand Down
38 changes: 38 additions & 0 deletions components/UnitFormDialog.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<script setup lang="ts">
import type { Input, Output } from "valibot";
import type { SubmissionHandler } from "vee-validate";
const props = defineProps<{
onSubmit?: SubmissionHandler<Output<typeof UnitFormSchema>>;
}>();
const model = defineModel<Input<typeof UnitFormSchema> | undefined>({
default: undefined,
});
const open = (list: Input<typeof UnitFormSchema>) => {
model.value = list;
};
const onSubmit = async (values: Output<typeof UnitFormSchema>, opts: any) => {
await props.onSubmit?.(values, opts);
model.value = undefined;
};
</script>

<template>
<BaseDialog
:model-value="!!model"
:title="model?.id ? `Edit ${model.title} category` : 'Create new category'"
@update:model-value="model = undefined"
>
<template #activator>
<slot name="activator" :open="open" />
</template>
<UnitForm
:initial-values="model"
:on-submit="onSubmit"
@cancel="model = undefined"
/>
</BaseDialog>
</template>
7 changes: 6 additions & 1 deletion pages/admin/categories.vue
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,12 @@ const deleteCategory = async (category: { id: string; title: string }) => {
<div class="flex flex-col items-center gap-4 lg:flex-row">
<h1 class="my-0 grow text-center text-5xl lg:text-left">Categories</h1>
<CategoryFormDialog v-model="dialogRef" :on-submit="submit" />
<BaseButton @click="createNewCategory">Add Category</BaseButton>
<BaseButton
v-if="session?.user?.permissions.includes(permissions.CategoriesCreate)"
@click="createNewCategory"
>
Add Category
</BaseButton>
</div>
<BaseTable
:headers="[
Expand Down
123 changes: 116 additions & 7 deletions pages/admin/units.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<script setup lang="ts">
import { useNotification } from "@kyvg/vue3-notification";
import { FetchError } from "ofetch";
import type { Input, Output } from "valibot";
definePageMeta({
middleware: () => {
const { session } = useAuth();
Expand All @@ -12,7 +16,93 @@ definePageMeta({
},
});
const { data: units } = await useFetch("/api/units");
const { notify } = useNotification();
const { session } = useAuth();
const { data: units, refresh: refreshUnits } = await useFetch("/api/units");
const dialogRef = ref<Input<typeof UnitFormSchema> | undefined>();
const createNewUnit = () => {
dialogRef.value = {
id: undefined,
title: "",
type: undefined as any,
abbreviation: "",
};
};
const submit = async (unit: Output<typeof UnitFormSchema>) => {
const id = dialogRef.value?.id;
try {
if (id) {
await $fetch(`/api/units/${id}`, {
method: "PUT",
body: unit,
});
} else {
await $fetch("/api/units", {
method: "POST",
body: unit,
});
}
await refreshUnits();
notify({
type: "success",
title: `Unit ${unit.title} ${id ? "edited" : "created"}.`,
text: `Unit has been successfully ${id ? "edited" : "created"}.`,
});
} catch (e) {
if (e instanceof FetchError) {
notify({
type: "error",
title: `Failed to ${id ? "edit" : "create"} category.`,
text: e.message,
});
return;
}
notify({
type: "error",
title: `Failed to ${id ? "edit" : "create"} category.`,
text: "An unknown error occurred.",
});
}
};
const deleteUnit = async (unit: { id: string; title: string }) => {
try {
await $fetch(`/api/units/${unit.id}`, {
method: "DELETE",
});
await refreshUnits();
notify({
type: "success",
title: `Unit ${unit.title} deleted.`,
text: "Unit has been successfully deleted.",
});
} catch (e) {
if (e instanceof FetchError) {
notify({
type: "error",
title: "Failed to delete unit.",
text: e.message,
});
return;
}
notify({
type: "error",
title: "Failed to delete unit.",
text: "An unknown error occurred.",
});
}
};
</script>

<template>
Expand All @@ -21,7 +111,13 @@ const { data: units } = await useFetch("/api/units");
>
<div class="flex flex-col items-center gap-4 lg:flex-row">
<h1 class="my-0 grow text-center text-5xl lg:text-left">Units</h1>
<BaseButton>Add Unit</BaseButton>
<UnitFormDialog v-model="dialogRef" :on-submit="submit" />
<BaseButton
v-if="session?.user?.permissions.includes(permissions.UnitsCreate)"
@click="createNewUnit"
>
Add Unit
</BaseButton>
</div>

<BaseTable
Expand Down Expand Up @@ -66,14 +162,27 @@ const { data: units } = await useFetch("/api/units");
<template #item[updatedAt]="{ item }">
{{ new Date(item.updatedAt).toLocaleString() }}
</template>
<template #item[trailing]>
<template #item[trailing]="{ item }">
<div class="box-border flex justify-end gap-2 py-1 pr-1">
<BaseButton spread="compact" variant="secondary">
<BaseButton
v-if="session?.user?.permissions.includes(permissions.UnitsUpdate)"
spread="compact"
variant="secondary"
@click="dialogRef = item"
>
<div class="i-material-symbols:edit h-6 w-6" />
</BaseButton>
<BaseButton spread="compact" variant="danger">
<div class="i-material-symbols:delete h-6 w-6" />
</BaseButton>
<ConfirmationDialog
v-if="session?.user?.permissions.includes(permissions.UnitsDelete)"
:on-confirm="() => deleteUnit(item)"
:reason="`Unit ${item.title} will be deleted.`"
>
<template #activator="{ open }">
<BaseButton spread="compact" variant="danger" @click="open">
<div class="i-material-symbols:delete h-6 w-6" />
</BaseButton>
</template>
</ConfirmationDialog>
</div>
</template>
</BaseTable>
Expand Down
1 change: 0 additions & 1 deletion server/api/categories/[id].delete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { getServerSession } from "#auth";
import { useValidatedParams } from "h3-valibot";
import { string, toTrimmed, objectAsync, uuid } from "valibot";
import { authOptions } from "../auth/[...]";
import permissions from "~/utils/permissions";

const ParametersSchema = objectAsync({
id: string("id parameter is required.", [
Expand Down
8 changes: 1 addition & 7 deletions server/api/categories/[id].put.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { getServerSession } from "#auth";
import { useValidatedBody, useValidatedParams } from "h3-valibot";
import { string, toTrimmed, objectAsync, uuid } from "valibot";
import { authOptions } from "../auth/[...]";
import permissions from "~/utils/permissions";
import CategorySchema from "~/server/schemas/CategorySchema";

const ParametersSchema = objectAsync({
id: string("id parameter is required.", [
Expand All @@ -26,10 +24,7 @@ export default defineEventHandler(async (event) => {
}

const { id } = await useValidatedParams(event, ParametersSchema);
const { title, slug, icon, order } = await useValidatedBody(
event,
CategorySchema,
);
const { title, slug, icon } = await useValidatedBody(event, CategorySchema);

await prisma.$transaction(async (tx) => {
const category = await tx.category.findUnique({
Expand All @@ -53,7 +48,6 @@ export default defineEventHandler(async (event) => {
title,
slug,
icon,
order,
},
});
});
Expand Down
8 changes: 1 addition & 7 deletions server/api/categories/index.post.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { getServerSession } from "#auth";
import { useValidatedBody } from "h3-valibot";
import { authOptions } from "../auth/[...]";
import CategorySchema from "~/server/schemas/CategorySchema";
import permissions from "~/utils/permissions";

export default defineEventHandler(async (event) => {
const session = await getServerSession(event, authOptions);
Expand All @@ -17,17 +15,13 @@ export default defineEventHandler(async (event) => {
});
}

const { title, slug, icon, order } = await useValidatedBody(
event,
CategorySchema,
);
const { title, slug, icon } = await useValidatedBody(event, CategorySchema);

await prisma.category.create({
data: {
title,
slug,
icon,
order,
},
});

Expand Down
Loading

0 comments on commit e714790

Please sign in to comment.