chore: refactor data management pages (#7107)

This commit is contained in:
Kuchenpirat
2026-02-24 18:23:33 +01:00
committed by GitHub
parent 03f849f20f
commit 282eedfe2b
19 changed files with 1457 additions and 2586 deletions

View File

@@ -95,6 +95,7 @@
<script lang="ts">
import { fieldTypes } from "~/composables/forms";
import { useGroups } from "~/composables/use-groups";
import { validators } from "~/composables/use-validators";
import type { GroupInDB } from "~/lib/api/types/user";
export default defineNuxtComponent({
@@ -136,7 +137,7 @@ export default defineNuxtComponent({
label: i18n.t("group.group-name"),
varName: "name",
type: fieldTypes.TEXT,
rules: ["required"],
rules: [validators.required],
},
],
data: {

View File

@@ -160,7 +160,7 @@ const createHouseholdForm = reactive({
label: i18n.t("household.household-name"),
varName: "name",
type: fieldTypes.TEXT,
rules: ["required"],
rules: [validators.required],
},
],
data: {

View File

@@ -1,245 +1,91 @@
<template>
<div>
<!-- Create Dialog -->
<BaseDialog
v-model="state.createDialog"
:title="$t('data-pages.categories.new-category')"
<GroupDataPage
:icon="$globals.icons.categories"
can-submit
@submit="createCategory"
>
<v-card-text>
<v-form ref="domNewCategoryForm">
<v-text-field
v-model="createTarget.name"
autofocus
:label="$t('general.name')"
:rules="[validators.required]"
/>
</v-form>
</v-card-text>
</BaseDialog>
<!-- Edit Dialog -->
<BaseDialog
v-model="state.editDialog"
:icon="$globals.icons.categories"
:title="$t('data-pages.categories.edit-category')"
:submit-text="$t('general.save')"
can-submit
@submit="editSaveCategory"
>
<v-card-text v-if="editTarget">
<div class="mt-4">
<v-text-field
v-model="editTarget.name"
:label="$t('general.name')"
/>
</div>
</v-card-text>
</BaseDialog>
<!-- Delete Dialog -->
<BaseDialog
v-model="state.deleteDialog"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteCategory"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
<p
v-if="deleteTarget"
class="mt-4 ml-4"
>
{{ deleteTarget.name }}
</p>
</v-card-text>
</BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="state.bulkDeleteDialog"
width="650px"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">
{{ $t('general.confirm-delete-generic-items') }}
</p>
<v-card variant="outlined">
<v-virtual-scroll
height="400"
item-height="25"
:items="bulkDeleteTarget"
>
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Data Table -->
<BaseCardSectionTitle
:icon="$globals.icons.categories"
section
:title="$t('data-pages.categories.category-data')"
/>
<CrudTable
v-model:headers="tableHeaders"
:table-headers="tableHeaders"
:table-config="tableConfig"
:data="categories || []"
:data="categoryStore.store.value || []"
:bulk-actions="[{ icon: $globals.icons.delete, text: $t('general.delete'), event: 'delete-selected' }]"
initial-sort="name"
@delete-one="deleteEventHandler"
@edit-one="editEventHandler"
@delete-selected="bulkDeleteEventHandler"
>
<template #button-row>
<BaseButton
create
@click="state.createDialog = true"
>
{{ $t("general.create") }}
</BaseButton>
</template>
</CrudTable>
:create-form="createForm"
:edit-form="editForm"
@create-one="handleCreate"
@edit-one="handleEdit"
@delete-one="categoryStore.actions.deleteOne"
@bulk-action="handleBulkAction"
/>
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { useCategoryStore } from "~/composables/store";
import { validators } from "~/composables/use-validators";
import { useCategoryStore, useCategoryData } from "~/composables/store";
import { fieldTypes } from "~/composables/forms";
import type { AutoFormItems } from "~/types/auto-forms";
import type { RecipeCategory } from "~/lib/api/types/recipe";
import type { TableHeaders, TableConfig } from "~/components/global/CrudTable.vue";
export default defineNuxtComponent({
setup() {
const i18n = useI18n();
const tableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
];
const state = reactive({
createDialog: false,
editDialog: false,
deleteDialog: false,
bulkDeleteDialog: false,
});
const categoryData = useCategoryData();
const categoryStore = useCategoryStore();
// ============================================================
// Create Category
async function createCategory() {
await categoryStore.actions.createOne({
name: categoryData.data.name,
slug: "",
});
categoryData.reset();
state.createDialog = false;
}
// ============================================================
// Edit Category
const editTarget = ref<RecipeCategory | null>(null);
function editEventHandler(item: RecipeCategory) {
state.editDialog = true;
editTarget.value = item;
}
async function editSaveCategory() {
if (!editTarget.value) {
return;
}
await categoryStore.actions.updateOne(editTarget.value);
state.editDialog = false;
}
// ============================================================
// Delete Category
const deleteTarget = ref<RecipeCategory | null>(null);
function deleteEventHandler(item: RecipeCategory) {
state.deleteDialog = true;
deleteTarget.value = item;
}
async function deleteCategory() {
if (!deleteTarget.value || deleteTarget.value.id === undefined) {
return;
}
await categoryStore.actions.deleteOne(deleteTarget.value.id);
state.deleteDialog = false;
}
// ============================================================
// Bulk Delete Category
const bulkDeleteTarget = ref<RecipeCategory[]>([]);
function bulkDeleteEventHandler(selection: RecipeCategory[]) {
bulkDeleteTarget.value = selection;
state.bulkDeleteDialog = true;
}
async function deleteSelected() {
const ids = bulkDeleteTarget.value.map(item => item.id).filter(id => !!id);
await categoryStore.actions.deleteMany(ids);
bulkDeleteTarget.value = [];
}
return {
state,
tableConfig,
tableHeaders,
categories: categoryStore.store,
validators,
// create
createTarget: categoryData.data,
createCategory,
// edit
editTarget,
editEventHandler,
editSaveCategory,
// delete
deleteTarget,
deleteEventHandler,
deleteCategory,
// bulk delete
bulkDeleteTarget,
bulkDeleteEventHandler,
deleteSelected,
};
const i18n = useI18n();
const tableConfig: TableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders: TableHeaders[] = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
];
const categoryStore = useCategoryStore();
// ============================================================
// Form items (shared)
const formItems = [
{
label: i18n.t("general.name"),
varName: "name",
type: fieldTypes.TEXT,
rules: [validators.required],
},
] as AutoFormItems;
// ============================================================
// Create
const createForm = reactive({
items: formItems,
data: { name: "" } as RecipeCategory,
});
async function handleCreate(createFormData: RecipeCategory) {
await categoryStore.actions.createOne(createFormData);
createForm.data.name = "";
}
// ============================================================
// Edit
const editForm = reactive({
items: formItems,
data: {} as RecipeCategory,
});
async function handleEdit(editFormData: RecipeCategory) {
await categoryStore.actions.updateOne(editFormData);
editForm.data = {} as RecipeCategory;
}
// ============================================================
// Bulk Actions
async function handleBulkAction(event: string, items: RecipeCategory[]) {
if (event === "delete-selected") {
const ids = items.filter(item => item.id != null).map(item => item.id!);
await categoryStore.actions.deleteMany(ids);
}
}
</script>

View File

@@ -79,170 +79,15 @@
</v-card-text>
</BaseDialog>
<!-- Create Dialog -->
<BaseDialog
v-model="createDialog"
:icon="$globals.icons.foods"
:title="$t('data-pages.foods.create-food')"
:submit-icon="$globals.icons.save"
:submit-text="$t('general.save')"
can-submit
@submit="createFood"
>
<v-card-text>
<v-form ref="domNewFoodForm">
<v-text-field
v-model="createTarget.name"
autofocus
:label="$t('general.name')"
:hint="$t('data-pages.foods.example-food-singular')"
:rules="[validators.required]"
/>
<v-text-field
v-model="createTarget.pluralName"
:label="$t('general.plural-name')"
:hint="$t('data-pages.foods.example-food-plural')"
/>
<v-text-field
v-model="createTarget.description"
:label="$t('recipe.description')"
/>
<v-autocomplete
v-model="createTarget.labelId"
clearable
:items="allLabels"
:custom-filter="normalizeFilter"
item-value="id"
item-title="name"
:label="$t('data-pages.foods.food-label')"
/>
<v-checkbox
v-model="createTarget.onHand"
hide-details
:label="$t('tool.on-hand')"
/>
<p class="text-caption mt-1">
{{ $t("data-pages.foods.on-hand-checkbox-label") }}
</p>
</v-form>
</v-card-text>
</BaseDialog>
<!-- Alias Sub-Dialog -->
<RecipeDataAliasManagerDialog
v-if="editTarget"
v-if="editForm.data"
v-model="aliasManagerDialog"
:data="editTarget"
:data="editForm.data"
@submit="updateFoodAlias"
@cancel="aliasManagerDialog = false"
/>
<!-- Edit Dialog -->
<BaseDialog
v-model="editDialog"
:icon="$globals.icons.foods"
:title="$t('data-pages.foods.edit-food')"
:submit-icon="$globals.icons.save"
:submit-text="$t('general.save')"
can-submit
@submit="editSaveFood"
>
<v-card-text v-if="editTarget">
<v-form ref="domEditFoodForm">
<v-text-field
v-model="editTarget.name"
:label="$t('general.name')"
:hint="$t('data-pages.foods.example-food-singular')"
:rules="[validators.required]"
/>
<v-text-field
v-model="editTarget.pluralName"
:label="$t('general.plural-name')"
:hint="$t('data-pages.foods.example-food-plural')"
/>
<v-text-field
v-model="editTarget.description"
:label="$t('recipe.description')"
/>
<v-autocomplete
v-model="editTarget.labelId"
clearable
:items="allLabels"
:custom-filter="normalizeFilter"
item-value="id"
item-title="name"
:label="$t('data-pages.foods.food-label')"
/>
<v-checkbox
v-model="editTarget.onHand"
hide-details
:label="$t('tool.on-hand')"
/>
<p class="text-caption mt-1">
{{ $t("data-pages.foods.on-hand-checkbox-label") }}
</p>
</v-form>
</v-card-text>
<template #custom-card-action>
<BaseButton
edit
@click="aliasManagerEventHandler"
>
{{ $t('data-pages.manage-aliases') }}
</BaseButton>
</template>
</BaseDialog>
<!-- Delete Dialog -->
<BaseDialog
v-model="deleteDialog"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteFood"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
<p
v-if="deleteTarget"
class="mt-4 ml-4"
>
{{ deleteTarget.name }}
</p>
</v-card-text>
</BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="bulkDeleteDialog"
width="650px"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">
{{ $t('general.confirm-delete-generic-items') }}
</p>
<v-card variant="outlined">
<v-virtual-scroll
height="400"
item-height="25"
:items="bulkDeleteTarget"
>
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Bulk Assign Labels Dialog -->
<BaseDialog
v-model="bulkAssignLabelDialog"
@@ -282,33 +127,24 @@
</v-card-text>
</BaseDialog>
<!-- Data Table -->
<BaseCardSectionTitle
<GroupDataPage
:icon="$globals.icons.foods"
section
:title="$t('data-pages.foods.food-data')"
/>
<CrudTable
v-model:headers="tableHeaders"
:table-headers="tableHeaders"
:table-config="tableConfig"
:data="foods || []"
:bulk-actions="[
{ icon: $globals.icons.delete, text: $t('general.delete'), event: 'delete-selected' },
{ icon: $globals.icons.tags, text: $t('data-pages.labels.assign-label'), event: 'assign-selected' },
]"
initial-sort="createdAt"
initial-sort-desc
@delete-one="deleteEventHandler"
@edit-one="editEventHandler"
@create-one="createEventHandler"
@delete-selected="bulkDeleteEventHandler"
@assign-selected="bulkAssignEventHandler"
:create-form="createForm"
:edit-form="editForm"
@create-one="handleCreate"
@edit-one="handleEdit"
@delete-one="foodStore.actions.deleteOne"
@bulk-action="handleBulkAction"
>
<template #button-row>
<BaseButton
create
@click="createDialog = true"
/>
<template #table-button-row>
<BaseButton @click="mergeDialog = true">
<template #icon>
{{ $globals.icons.externalLink }}
@@ -316,6 +152,7 @@
{{ $t('data-pages.combine') }}
</BaseButton>
</template>
<template #[`item.label`]="{ item }">
<MultiPurposeLabel
v-if="item.label"
@@ -324,15 +161,18 @@
{{ item.label.name }}
</MultiPurposeLabel>
</template>
<template #[`item.onHand`]="{ item }">
<v-icon :color="item.onHand ? 'success' : undefined">
{{ item.onHand ? $globals.icons.check : $globals.icons.close }}
</v-icon>
</template>
<template #[`item.createdAt`]="{ item }">
{{ item.createdAt ? $d(new Date(item.createdAt)) : '' }}
</template>
<template #button-bottom>
<template #table-button-bottom>
<BaseButton @click="seedDialog = true">
<template #icon>
{{ $globals.icons.database }}
@@ -340,11 +180,20 @@
{{ $t('data-pages.seed') }}
</BaseButton>
</template>
</CrudTable>
<template #edit-dialog-custom-action>
<BaseButton
edit
@click="aliasManagerDialog = true"
>
{{ $t('data-pages.manage-aliases') }}
</BaseButton>
</template>
</GroupDataPage>
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import type { LocaleObject } from "@nuxtjs/i18n";
import RecipeDataAliasManagerDialog from "~/components/Domain/Recipe/RecipeDataAliasManagerDialog.vue";
import { validators } from "~/composables/use-validators";
@@ -355,7 +204,9 @@ import { useLocales } from "~/composables/use-locales";
import { normalizeFilter } from "~/composables/use-utils";
import { useFoodStore, useLabelStore } from "~/composables/store";
import type { MultiPurposeLabelOut } from "~/lib/api/types/labels";
import type { VForm } from "~/types/auto-forms";
import type { AutoFormItems } from "~/types/auto-forms";
import type { TableHeaders, TableConfig } from "~/components/global/CrudTable.vue";
import { fieldTypes } from "~/composables/forms";
interface CreateIngredientFoodWithOnHand extends CreateIngredientFood {
onHand: boolean;
@@ -365,315 +216,256 @@ interface CreateIngredientFoodWithOnHand extends CreateIngredientFood {
interface IngredientFoodWithOnHand extends IngredientFood {
onHand: boolean;
}
export default defineNuxtComponent({
components: { MultiPurposeLabel, RecipeDataAliasManagerDialog },
setup() {
const userApi = useUserApi();
const i18n = useI18n();
const auth = useMealieAuth();
const tableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
{
text: i18n.t("general.plural-name"),
value: "pluralName",
show: true,
sortable: true,
},
{
text: i18n.t("recipe.description"),
value: "description",
show: true,
},
{
text: i18n.t("shopping-list.label"),
value: "label",
show: true,
sortable: true,
sort: (label1: MultiPurposeLabelOut | null, label2: MultiPurposeLabelOut | null) => {
const label1Name = label1?.name || "";
const label2Name = label2?.name || "";
return label1Name.localeCompare(label2Name);
},
},
{
text: i18n.t("tool.on-hand"),
value: "onHand",
show: true,
sortable: true,
},
{
text: i18n.t("general.date-added"),
value: "createdAt",
show: false,
sortable: true,
},
];
const userHousehold = computed(() => auth.user.value?.householdSlug || "");
const foodStore = useFoodStore();
const foods = computed(() => foodStore.store.value.map((food) => {
const onHand = food.householdsWithIngredientFood?.includes(userHousehold.value) || false;
return { ...food, onHand } as IngredientFoodWithOnHand;
}));
// ===============================================================
// Food Creator
const domNewFoodForm = ref<VForm>();
const createDialog = ref(false);
const createTarget = ref<CreateIngredientFoodWithOnHand>({
name: "",
onHand: false,
householdsWithIngredientFood: [],
});
function createEventHandler() {
createDialog.value = true;
}
async function createFood() {
if (!createTarget.value || !createTarget.value.name) {
return;
}
if (createTarget.value.onHand) {
createTarget.value.householdsWithIngredientFood = [userHousehold.value];
}
// @ts-expect-error the createOne function erroneously expects an id because it uses the IngredientFood type
await foodStore.actions.createOne(createTarget.value);
createDialog.value = false;
domNewFoodForm.value?.reset();
createTarget.value = {
name: "",
onHand: false,
householdsWithIngredientFood: [],
};
}
// ===============================================================
// Food Editor
const editDialog = ref(false);
const editTarget = ref<IngredientFoodWithOnHand | null>(null);
function editEventHandler(item: IngredientFoodWithOnHand) {
editTarget.value = item;
editTarget.value.onHand = item.householdsWithIngredientFood?.includes(userHousehold.value) || false;
editDialog.value = true;
}
async function editSaveFood() {
if (!editTarget.value) {
return;
}
if (editTarget.value.onHand && !editTarget.value.householdsWithIngredientFood?.includes(userHousehold.value)) {
if (!editTarget.value.householdsWithIngredientFood) {
editTarget.value.householdsWithIngredientFood = [userHousehold.value];
}
else {
editTarget.value.householdsWithIngredientFood.push(userHousehold.value);
}
}
else if (!editTarget.value.onHand && editTarget.value.householdsWithIngredientFood?.includes(userHousehold.value)) {
editTarget.value.householdsWithIngredientFood = editTarget.value.householdsWithIngredientFood.filter(
household => household !== userHousehold.value,
);
}
await foodStore.actions.updateOne(editTarget.value);
editDialog.value = false;
}
// ===============================================================
// Food Delete
const deleteDialog = ref(false);
const deleteTarget = ref<IngredientFoodWithOnHand | null>(null);
function deleteEventHandler(item: IngredientFoodWithOnHand) {
deleteTarget.value = item;
deleteDialog.value = true;
}
async function deleteFood() {
if (!deleteTarget.value) {
return;
}
await foodStore.actions.deleteOne(deleteTarget.value.id);
deleteDialog.value = false;
}
const bulkDeleteDialog = ref(false);
const bulkDeleteTarget = ref<IngredientFoodWithOnHand[]>([]);
function bulkDeleteEventHandler(selection: IngredientFoodWithOnHand[]) {
bulkDeleteTarget.value = selection;
bulkDeleteDialog.value = true;
}
async function deleteSelected() {
const ids = bulkDeleteTarget.value.map(item => item.id);
await foodStore.actions.deleteMany(ids);
bulkDeleteTarget.value = [];
}
// ============================================================
// Alias Manager
const aliasManagerDialog = ref(false);
function aliasManagerEventHandler() {
aliasManagerDialog.value = true;
}
function updateFoodAlias(newAliases: IngredientFoodAlias[]) {
if (!editTarget.value) {
return;
}
editTarget.value.aliases = newAliases;
aliasManagerDialog.value = false;
}
// ============================================================
// Merge Foods
const mergeDialog = ref(false);
const fromFood = ref<IngredientFoodWithOnHand | null>(null);
const toFood = ref<IngredientFoodWithOnHand | null>(null);
const canMerge = computed(() => {
return fromFood.value && toFood.value && fromFood.value.id !== toFood.value.id;
});
async function mergeFoods() {
if (!canMerge.value || !fromFood.value || !toFood.value) {
return;
}
const { data } = await userApi.foods.merge(fromFood.value.id, toFood.value.id);
if (data) {
foodStore.actions.refresh();
}
}
// ============================================================
// Labels
const { store: allLabels } = useLabelStore();
// ============================================================
// Seed
const seedDialog = ref(false);
const locale = ref("");
const { locales: LOCALES, locale: currentLocale } = useLocales();
onMounted(() => {
locale.value = currentLocale.value;
});
const locales = LOCALES.filter(locale =>
(i18n.locales.value as LocaleObject[]).map(i18nLocale => i18nLocale.code).includes(locale.value as any),
);
async function seedDatabase() {
const { data } = await userApi.seeders.foods({ locale: locale.value });
if (data) {
foodStore.actions.refresh();
}
}
// ============================================================
// Bulk Assign Labels
const bulkAssignLabelDialog = ref(false);
const bulkAssignTarget = ref<IngredientFoodWithOnHand[]>([]);
const bulkAssignLabelId = ref<string | undefined>();
function bulkAssignEventHandler(selection: IngredientFoodWithOnHand[]) {
bulkAssignTarget.value = selection;
bulkAssignLabelDialog.value = true;
}
async function assignSelected() {
if (!bulkAssignLabelId.value) {
return;
}
for (const item of bulkAssignTarget.value) {
item.labelId = bulkAssignLabelId.value;
await foodStore.actions.updateOne(item);
}
bulkAssignTarget.value = [];
bulkAssignLabelId.value = undefined;
foodStore.actions.refresh();
}
return {
tableConfig,
tableHeaders,
foods,
allLabels,
validators,
normalizeFilter,
// Create
createDialog,
domNewFoodForm,
createEventHandler,
createFood,
createTarget,
// Edit
editDialog,
editEventHandler,
editSaveFood,
editTarget,
// Delete
deleteEventHandler,
deleteDialog,
deleteFood,
deleteTarget,
bulkDeleteDialog,
bulkDeleteTarget,
bulkDeleteEventHandler,
deleteSelected,
// Alias Manager
aliasManagerDialog,
aliasManagerEventHandler,
updateFoodAlias,
// Merge
canMerge,
mergeFoods,
mergeDialog,
fromFood,
toFood,
// Seed Data
locale,
locales,
seedDialog,
seedDatabase,
// Bulk Assign Labels
bulkAssignLabelDialog,
bulkAssignTarget,
bulkAssignLabelId,
bulkAssignEventHandler,
assignSelected,
};
const userApi = useUserApi();
const i18n = useI18n();
const auth = useMealieAuth();
const tableConfig: TableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders: TableHeaders[] = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
{
text: i18n.t("general.plural-name"),
value: "pluralName",
show: true,
sortable: true,
},
{
text: i18n.t("recipe.description"),
value: "description",
show: true,
},
{
text: i18n.t("shopping-list.label"),
value: "label",
show: true,
sortable: true,
sort: (label1: MultiPurposeLabelOut | null, label2: MultiPurposeLabelOut | null) => {
const label1Name = label1?.name || "";
const label2Name = label2?.name || "";
return label1Name.localeCompare(label2Name);
},
},
{
text: i18n.t("tool.on-hand"),
value: "onHand",
show: true,
sortable: true,
},
{
text: i18n.t("general.date-added"),
value: "createdAt",
show: false,
sortable: true,
},
];
const userHousehold = computed(() => auth.user.value?.householdSlug || "");
const foodStore = useFoodStore();
const foods = computed(() => foodStore.store.value.map((food) => {
const onHand = food.householdsWithIngredientFood?.includes(userHousehold.value) || false;
return { ...food, onHand } as IngredientFoodWithOnHand;
}));
// ============================================================
// Labels
const { store: allLabels } = useLabelStore();
const labelOptions = computed(() => allLabels.value.map(label => ({ text: label.name, value: label.id })) || []);
// ============================================================
// Form items (shared)
const formItems = computed<AutoFormItems>(() => [
{
label: i18n.t("general.name"),
varName: "name",
type: fieldTypes.TEXT,
rules: [validators.required],
},
{
label: i18n.t("general.plural-name"),
varName: "pluralName",
type: fieldTypes.TEXT,
},
{
label: i18n.t("recipe.description"),
varName: "description",
type: fieldTypes.TEXT,
},
{
label: i18n.t("data-pages.foods.food-label"),
varName: "labelId",
type: fieldTypes.SELECT,
options: labelOptions.value,
},
{
label: i18n.t("tool.on-hand"),
varName: "onHand",
type: fieldTypes.BOOLEAN,
hint: i18n.t("data-pages.foods.on-hand-checkbox-label"),
},
]);
// ===============================================================
// Create
const createForm = reactive({
get items() {
return formItems.value;
},
data: { name: "", onHand: false, householdsWithIngredientFood: [] } as CreateIngredientFoodWithOnHand,
});
async function handleCreate() {
if (!createForm.data || !createForm.data.name) {
return;
}
if (createForm.data.onHand) {
createForm.data.householdsWithIngredientFood = [userHousehold.value];
}
// @ts-expect-error the createOne function erroneously expects an id because it uses the IngredientFood type
await foodStore.actions.createOne(createForm.data);
createForm.data = {
name: "",
onHand: false,
householdsWithIngredientFood: [],
};
}
// ===============================================================
// Edit
const editForm = reactive({
get items() {
return formItems.value;
},
data: {} as IngredientFoodWithOnHand,
});
async function handleEdit() {
if (!editForm.data) {
return;
}
if (!editForm.data.householdsWithIngredientFood) {
editForm.data.householdsWithIngredientFood = [];
}
if (editForm.data.onHand && !editForm.data.householdsWithIngredientFood.includes(userHousehold.value)) {
editForm.data.householdsWithIngredientFood.push(userHousehold.value);
}
else if (!editForm.data.onHand && editForm.data.householdsWithIngredientFood.includes(userHousehold.value)) {
const idx = editForm.data.householdsWithIngredientFood.indexOf(userHousehold.value);
if (idx !== -1) editForm.data.householdsWithIngredientFood.splice(idx, 1);
}
await foodStore.actions.updateOne(editForm.data);
editForm.data = {} as IngredientFoodWithOnHand;
}
// ============================================================
// Bulk Actions
async function handleBulkAction(event: string, items: IngredientFoodWithOnHand[]) {
if (event === "delete-selected") {
const ids = items.map(item => item.id);
await foodStore.actions.deleteMany(ids);
}
else if (event === "assign-selected") {
bulkAssignEventHandler(items);
}
}
// ============================================================
// Alias Manager
const aliasManagerDialog = ref(false);
function updateFoodAlias(newAliases: IngredientFoodAlias[]) {
if (!editForm.data) {
return;
}
editForm.data.aliases = newAliases;
aliasManagerDialog.value = false;
}
// ============================================================
// Merge Foods
const mergeDialog = ref(false);
const fromFood = ref<IngredientFoodWithOnHand | null>(null);
const toFood = ref<IngredientFoodWithOnHand | null>(null);
const canMerge = computed(() => {
return fromFood.value && toFood.value && fromFood.value.id !== toFood.value.id;
});
async function mergeFoods() {
if (!canMerge.value || !fromFood.value || !toFood.value) {
return;
}
const { data } = await userApi.foods.merge(fromFood.value.id, toFood.value.id);
if (data) {
foodStore.actions.refresh();
}
}
// ============================================================
// Seed
const seedDialog = ref(false);
const locale = ref("");
const { locales: LOCALES, locale: currentLocale } = useLocales();
onMounted(() => {
locale.value = currentLocale.value;
});
const locales = LOCALES.filter(locale =>
(i18n.locales.value as LocaleObject[]).map(i18nLocale => i18nLocale.code).includes(locale.value as any),
);
async function seedDatabase() {
const { data } = await userApi.seeders.foods({ locale: locale.value });
if (data) {
foodStore.actions.refresh();
}
}
// ============================================================
// Bulk Assign Labels
const bulkAssignLabelDialog = ref(false);
const bulkAssignTarget = ref<IngredientFoodWithOnHand[]>([]);
const bulkAssignLabelId = ref<string | undefined>();
function bulkAssignEventHandler(selection: IngredientFoodWithOnHand[]) {
bulkAssignTarget.value = selection;
bulkAssignLabelDialog.value = true;
}
async function assignSelected() {
if (!bulkAssignLabelId.value) {
return;
}
for (const item of bulkAssignTarget.value) {
item.labelId = bulkAssignLabelId.value;
await foodStore.actions.updateOne(item);
}
bulkAssignTarget.value = [];
bulkAssignLabelId.value = undefined;
foodStore.actions.refresh();
}
</script>

View File

@@ -1,99 +1,5 @@
<template>
<div>
<!-- Create New Dialog -->
<BaseDialog
v-model="state.createDialog"
:title="$t('data-pages.labels.new-label')"
:icon="$globals.icons.tags"
can-submit
@submit="createLabel"
>
<v-card-text>
<MultiPurposeLabel :label="createLabelData" />
<div class="mt-4">
<v-text-field
v-model="createLabelData.name"
:label="$t('general.name')"
/>
<InputColor v-model="createLabelData.color!" />
</div>
</v-card-text>
</BaseDialog>
<!-- Edit Dialog -->
<BaseDialog
v-model="state.editDialog"
:icon="$globals.icons.tags"
:title="$t('data-pages.labels.edit-label')"
:submit-icon="$globals.icons.save"
:submit-text="$t('general.save')"
can-submit
@submit="editSaveLabel"
>
<v-card-text v-if="editLabel">
<MultiPurposeLabel :label="editLabel" />
<div class="mt-4">
<v-text-field
v-model="editLabel.name"
:label="$t('general.name')"
/>
<InputColor v-model="editLabel.color!" />
</div>
</v-card-text>
</BaseDialog>
<!-- Delete Dialog -->
<BaseDialog
v-model="state.deleteDialog"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteLabel"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
<v-row>
<MultiPurposeLabel
v-if="deleteTarget"
class="mt-4 ml-4 mb-1"
:label="deleteTarget"
/>
</v-row>
</v-card-text>
</BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="state.bulkDeleteDialog"
width="650px"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">
{{ $t('general.confirm-delete-generic-items') }}
</p>
<v-card variant="outlined">
<v-virtual-scroll
height="400"
item-height="25"
:items="bulkDeleteTarget"
>
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Seed Dialog -->
<BaseDialog
v-model="seedDialog"
@@ -127,7 +33,7 @@
</v-autocomplete>
<v-alert
v-if="labels && labels.length > 0"
v-if="labelStore.store.value && labelStore.store.value.length > 0"
type="error"
class="mb-0 text-body-2"
>
@@ -136,30 +42,20 @@
</v-card-text>
</BaseDialog>
<!-- Data Table -->
<BaseCardSectionTitle
<GroupDataPage
:icon="$globals.icons.tags"
section
:title="$t('data-pages.labels.labels')"
/>
<CrudTable
v-model:headers="tableHeaders"
:table-headers="tableHeaders"
:table-config="tableConfig"
:data="labels || []"
:data="labelStore.store.value || []"
:bulk-actions="[{ icon: $globals.icons.delete, text: $t('general.delete'), event: 'delete-selected' }]"
initial-sort="name"
@delete-one="deleteEventHandler"
@edit-one="editEventHandler"
@delete-selected="bulkDeleteEventHandler"
:create-form="createForm"
:edit-form="editForm"
@create-one="handleCreate"
@edit-one="handleEdit"
@delete-one="labelStore.actions.deleteOne"
@bulk-action="handleBulkAction"
>
<template #button-row>
<BaseButton
create
@click="state.createDialog = true"
>
{{ $t("general.create") }}
</BaseButton>
</template>
<template #[`item.name`]="{ item }">
<MultiPurposeLabel
v-if="item"
@@ -168,7 +64,12 @@
{{ item.name }}
</MultiPurposeLabel>
</template>
<template #button-bottom>
<template #create-dialog-top>
<MultiPurposeLabel :label="createForm.data" class="my-2" />
</template>
<template #table-button-bottom>
<BaseButton @click="seedDialog = true">
<template #icon>
{{ $globals.icons.database }}
@@ -176,172 +77,114 @@
{{ $t('data-pages.seed') }}
</BaseButton>
</template>
</CrudTable>
</GroupDataPage>
</div>
</template>
<script lang="ts">
import type { LocaleObject } from "@nuxtjs/i18n";
<script setup lang="ts">
import { validators } from "~/composables/use-validators";
import { useUserApi } from "~/composables/api";
import MultiPurposeLabel from "~/components/Domain/ShoppingList/MultiPurposeLabel.vue";
import { fieldTypes } from "~/composables/forms";
import type { MultiPurposeLabelSummary } from "~/lib/api/types/labels";
import type { AutoFormItems } from "~/types/auto-forms";
import { useLocales } from "~/composables/use-locales";
import { normalizeFilter } from "~/composables/use-utils";
import { useLabelData, useLabelStore } from "~/composables/store";
import { useLabelStore } from "~/composables/store";
import type { TableHeaders, TableConfig } from "~/components/global/CrudTable.vue";
export default defineNuxtComponent({
components: { MultiPurposeLabel },
setup() {
const userApi = useUserApi();
const i18n = useI18n();
const userApi = useUserApi();
const i18n = useI18n();
const tableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
];
const state = reactive({
createDialog: false,
editDialog: false,
deleteDialog: false,
bulkDeleteDialog: false,
});
// ============================================================
// Labels
const labelData = useLabelData();
const labelStore = useLabelStore();
// Create
async function createLabel() {
await labelStore.actions.createOne(labelData.data);
labelData.reset();
state.createDialog = false;
}
// Delete
const deleteTarget = ref<MultiPurposeLabelSummary | null>(null);
function deleteEventHandler(item: MultiPurposeLabelSummary) {
state.deleteDialog = true;
deleteTarget.value = item;
}
async function deleteLabel() {
if (!deleteTarget.value) {
return;
}
await labelStore.actions.deleteOne(deleteTarget.value.id);
state.deleteDialog = false;
}
// Bulk Delete
const bulkDeleteTarget = ref<MultiPurposeLabelSummary[]>([]);
function bulkDeleteEventHandler(selection: MultiPurposeLabelSummary[]) {
bulkDeleteTarget.value = selection;
state.bulkDeleteDialog = true;
}
async function deleteSelected() {
const ids = bulkDeleteTarget.value.map(item => item.id);
await labelStore.actions.deleteMany(ids);
bulkDeleteTarget.value = [];
}
// Edit
const editLabel = ref<MultiPurposeLabelSummary | null>(null);
function editEventHandler(item: MultiPurposeLabelSummary) {
state.editDialog = true;
editLabel.value = item;
if (!editLabel.value.color) {
editLabel.value.color = "#959595";
}
}
async function editSaveLabel() {
if (!editLabel.value) {
return;
}
await labelStore.actions.updateOne(editLabel.value);
state.editDialog = false;
}
// ============================================================
// Seed
const seedDialog = ref(false);
const locale = ref("");
const { locales: LOCALES, locale: currentLocale } = useLocales();
onMounted(() => {
locale.value = currentLocale.value;
});
const locales = LOCALES.filter(locale =>
(i18n.locales.value as LocaleObject[]).map(i18nLocale => i18nLocale.code).includes(locale.value),
);
async function seedDatabase() {
const { data } = await userApi.seeders.labels({ locale: locale.value });
if (data) {
labelStore.actions.refresh();
}
}
return {
state,
tableConfig,
tableHeaders,
labels: labelStore.store,
validators,
normalizeFilter,
// create
createLabel,
createLabelData: labelData.data,
// edit
editLabel,
editEventHandler,
editSaveLabel,
// delete
deleteEventHandler,
deleteLabel,
deleteTarget,
bulkDeleteEventHandler,
deleteSelected,
bulkDeleteTarget,
// Seed
seedDatabase,
locales,
locale,
seedDialog,
};
const tableConfig: TableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders: TableHeaders[] = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
];
const labelStore = useLabelStore();
// ============================================================
// Form items (shared)
const formItems: AutoFormItems = [
{
label: i18n.t("general.name"),
varName: "name",
type: fieldTypes.TEXT,
rules: [validators.required],
},
{
label: i18n.t("general.color"),
varName: "color",
type: fieldTypes.COLOR,
},
];
// ============================================================
// Create
const createForm = reactive({
items: formItems,
data: {
name: "",
color: "",
} as MultiPurposeLabelSummary,
});
async function handleCreate(createFormData: MultiPurposeLabelSummary) {
await labelStore.actions.createOne(createFormData);
createForm.data = { name: "", color: "#7417BE" } as MultiPurposeLabelSummary;
}
// ============================================================
// Edit
const editForm = reactive({
items: formItems,
data: {} as MultiPurposeLabelSummary,
});
async function handleEdit(editFormData: MultiPurposeLabelSummary) {
await labelStore.actions.updateOne(editFormData);
editForm.data = {} as MultiPurposeLabelSummary;
}
// ============================================================
// Bulk Actions
async function handleBulkAction(event: string, items: MultiPurposeLabelSummary[]) {
if (event === "delete-selected") {
const ids = items.filter(item => item.id != null).map(item => item.id!);
await labelStore.actions.deleteMany(ids);
}
}
// ============================================================
// Seed
const seedDialog = ref(false);
const locale = ref("");
const { locales: locales, locale: currentLocale } = useLocales();
onMounted(() => {
locale.value = currentLocale.value;
});
async function seedDatabase() {
const { data } = await userApi.seeders.labels({ locale: locale.value });
if (data) {
labelStore.actions.refresh();
}
}
</script>

View File

@@ -1,286 +1,121 @@
<template>
<div>
<!-- Create Dialog -->
<BaseDialog
v-model="state.createDialog"
:title="$t('data-pages.recipe-actions.new-recipe-action')"
:icon="$globals.icons.linkVariantPlus"
can-submit
@submit="createAction"
>
<v-card-text>
<v-form ref="domNewActionForm">
<v-text-field
v-model="createTarget.title"
autofocus
:label="$t('general.title')"
:rules="[validators.required]"
/>
<v-text-field
v-model="createTarget.url"
:label="$t('general.url')"
:rules="[validators.required]"
/>
<v-select
v-model="createTarget.actionType"
:items="actionTypeOptions"
:label="$t('data-pages.recipe-actions.action-type')"
:rules="[validators.required]"
/>
</v-form>
</v-card-text>
</BaseDialog>
<!-- Edit Dialog -->
<BaseDialog
v-model="state.editDialog"
:icon="$globals.icons.linkVariantPlus"
:title="$t('data-pages.recipe-actions.edit-recipe-action')"
:submit-text="$t('general.save')"
can-submit
@submit="editSaveAction"
>
<v-card-text v-if="editTarget">
<div class="mt-4">
<v-text-field
v-model="editTarget.title"
:label="$t('general.title')"
/>
</div>
<div class="mt-4">
<v-text-field
v-model="editTarget.url"
:label="$t('general.url')"
/>
</div>
<div class="mt-4">
<v-select
v-model="editTarget.actionType"
:items="actionTypeOptions"
:label="$t('data-pages.recipe-actions.action-type')"
/>
</div>
</v-card-text>
</BaseDialog>
<!-- Delete Dialog -->
<BaseDialog
v-model="state.deleteDialog"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteAction"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
<p
v-if="deleteTarget"
class="mt-4 ml-4"
>
{{ deleteTarget.title }}
</p>
</v-card-text>
</BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="state.bulkDeleteDialog"
width="650px"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">
{{ $t('general.confirm-delete-generic-items') }}
</p>
<v-card variant="outlined">
<v-virtual-scroll
height="400"
item-height="25"
:items="bulkDeleteTarget"
>
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Data Table -->
<BaseCardSectionTitle
:icon="$globals.icons.linkVariantPlus"
section
:title="$t('data-pages.recipe-actions.recipe-actions-data')"
/>
<CrudTable
v-model:headers="tableHeaders"
<GroupDataPage
:icon="$globals.icons.categories"
:title="$t('data-pages.categories.category-data')"
:table-headers="tableHeaders"
:table-config="tableConfig"
:data="actions || []"
:data="actionStore.recipeActions.value || []"
:bulk-actions="[{ icon: $globals.icons.delete, text: $t('general.delete'), event: 'delete-selected' }]"
:create-form="createForm"
:edit-form="editForm"
initial-sort="title"
@delete-one="deleteEventHandler"
@edit-one="editEventHandler"
@delete-selected="bulkDeleteEventHandler"
>
<template #button-row>
<BaseButton
create
@click="state.createDialog = true"
>
{{ $t("general.create") }}
</BaseButton>
</template>
</CrudTable>
@create-one="handleCreate"
@edit-one="handleEdit"
@delete-one="actionStore.actions.deleteOne"
@bulk-action="handleBulkAction"
/>
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { validators } from "~/composables/use-validators";
import { useGroupRecipeActions, useGroupRecipeActionData } from "~/composables/use-group-recipe-actions";
import { useGroupRecipeActions } from "~/composables/use-group-recipe-actions";
import type { GroupRecipeActionOut } from "~/lib/api/types/household";
import type { TableHeaders, TableConfig } from "~/components/global/CrudTable.vue";
import type { AutoFormItems } from "~/types/auto-forms";
import { fieldTypes } from "~/composables/forms";
export default defineNuxtComponent({
setup() {
const i18n = useI18n();
const i18n = useI18n();
const tableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.title"),
value: "title",
show: true,
sortable: true,
},
{
text: i18n.t("general.url"),
value: "url",
show: true,
},
{
text: i18n.t("data-pages.recipe-actions.action-type"),
value: "actionType",
show: true,
sortable: true,
},
];
const state = reactive({
createDialog: false,
editDialog: false,
deleteDialog: false,
bulkDeleteDialog: false,
});
const actionData = useGroupRecipeActionData();
const actionStore = useGroupRecipeActions(null, null);
const actionTypeOptions = ["link", "post"];
// ============================================================
// Create Action
async function createAction() {
await actionStore.actions.createOne({
actionType: actionData.data.actionType,
title: actionData.data.title,
url: actionData.data.url,
} as GroupRecipeActionOut);
actionData.reset();
state.createDialog = false;
}
// ============================================================
// Edit Action
const editTarget = ref<GroupRecipeActionOut | null>(null);
function editEventHandler(item: GroupRecipeActionOut) {
state.editDialog = true;
editTarget.value = item;
}
async function editSaveAction() {
if (!editTarget.value) {
return;
}
await actionStore.actions.updateOne(editTarget.value);
state.editDialog = false;
}
// ============================================================
// Delete Action
const deleteTarget = ref<GroupRecipeActionOut | null>(null);
function deleteEventHandler(item: GroupRecipeActionOut) {
state.deleteDialog = true;
deleteTarget.value = item;
}
async function deleteAction() {
if (!deleteTarget.value || deleteTarget.value.id === undefined) {
return;
}
await actionStore.actions.deleteOne(deleteTarget.value.id);
state.deleteDialog = false;
}
// ============================================================
// Bulk Delete Action
const bulkDeleteTarget = ref<GroupRecipeActionOut[]>([]);
function bulkDeleteEventHandler(selection: GroupRecipeActionOut[]) {
bulkDeleteTarget.value = selection;
state.bulkDeleteDialog = true;
}
async function deleteSelected() {
const ids = bulkDeleteTarget.value.map(item => item.id);
await actionStore.actions.deleteMany(ids);
bulkDeleteTarget.value = [];
}
return {
state,
tableConfig,
tableHeaders,
actionTypeOptions,
actions: actionStore.recipeActions,
validators,
// create
createTarget: actionData.data,
createAction,
// edit
editTarget,
editEventHandler,
editSaveAction,
// delete
deleteTarget,
deleteEventHandler,
deleteAction,
// bulk delete
bulkDeleteTarget,
bulkDeleteEventHandler,
deleteSelected,
};
const tableConfig: TableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders: TableHeaders[] = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.title"),
value: "title",
show: true,
sortable: true,
},
{
text: i18n.t("general.url"),
value: "url",
show: true,
},
{
text: i18n.t("data-pages.recipe-actions.action-type"),
value: "actionType",
show: true,
sortable: true,
},
];
const actionStore = useGroupRecipeActions(null, null);
// ============================================================
// Form items (shared)
const formItems: AutoFormItems = [
{
label: i18n.t("general.title"),
varName: "title",
type: fieldTypes.TEXT,
rules: [validators.required],
},
{
label: i18n.t("general.url"),
varName: "url",
type: fieldTypes.TEXT,
rules: [validators.required, validators.url],
},
{
label: i18n.t("data-pages.recipe-actions.action-type"),
varName: "actionType",
type: fieldTypes.SELECT,
options: [{ text: "link" }, { text: "post" }],
rules: [validators.required],
},
];
// ============================================================
// Create
const createForm = reactive({
items: formItems,
data: {} as GroupRecipeActionOut,
});
async function handleCreate() {
await actionStore.actions.createOne(createForm.data);
createForm.data = {} as GroupRecipeActionOut;
}
// ============================================================
// Edit Action
const editForm = reactive({
items: formItems,
data: {} as GroupRecipeActionOut,
});
async function handleEdit(editFormData: GroupRecipeActionOut) {
await actionStore.actions.updateOne(editFormData);
editForm.data = {} as GroupRecipeActionOut;
}
// ============================================================
// Bulk Actions
async function handleBulkAction(event: string, items: GroupRecipeActionOut[]) {
console.log("Bulk Action Event:", event, "Items:", items);
if (event === "delete-selected") {
const ids = items.filter(item => item.id != null).map(item => item.id!);
await actionStore.actions.deleteMany(ids);
}
}
</script>

View File

@@ -1,248 +1,92 @@
<template>
<div>
<!-- Create Dialog -->
<BaseDialog
v-model="state.createDialog"
:title="$t('data-pages.tags.new-tag')"
<GroupDataPage
:icon="$globals.icons.tags"
can-submit
@submit="createTag"
>
<v-card-text>
<v-form ref="domNewTagForm">
<v-text-field
v-model="createTarget.name"
autofocus
:label="$t('general.name')"
:rules="[validators.required]"
/>
</v-form>
</v-card-text>
</BaseDialog>
<!-- Edit Dialog -->
<BaseDialog
v-model="state.editDialog"
:icon="$globals.icons.tags"
:title="$t('data-pages.tags.edit-tag')"
:submit-icon="$globals.icons.save"
:submit-text="$t('general.save')"
can-submit
@submit="editSaveTag"
>
<v-card-text v-if="editTarget">
<div class="mt-4">
<v-text-field
v-model="editTarget.name"
:label="$t('general.name')"
/>
</div>
</v-card-text>
</BaseDialog>
<!-- Delete Dialog -->
<BaseDialog
v-model="state.deleteDialog"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteTag"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
<p
v-if="deleteTarget"
class="mt-4 ml-4"
>
{{ deleteTarget.name }}
</p>
</v-card-text>
</BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="state.bulkDeleteDialog"
width="650px"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">
{{ $t('general.confirm-delete-generic-items') }}
</p>
<v-card variant="outlined">
<v-virtual-scroll
height="400"
item-height="25"
:items="bulkDeleteTarget"
>
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Data Table -->
<BaseCardSectionTitle
:icon="$globals.icons.tags"
section
:title="$t('data-pages.tags.tag-data')"
/>
<CrudTable
v-model:headers="tableHeaders"
:table-headers="tableHeaders"
:table-config="tableConfig"
:data="tags || []"
:data="tagStore.store.value || []"
:bulk-actions="[{ icon: $globals.icons.delete, text: $t('general.delete'), event: 'delete-selected' }]"
initial-sort="name"
@delete-one="deleteEventHandler"
@edit-one="editEventHandler"
@delete-selected="bulkDeleteEventHandler"
>
<template #button-row>
<BaseButton
create
@click="state.createDialog = true"
>
{{ $t("general.create") }}
</BaseButton>
</template>
</CrudTable>
:create-form="createForm"
:edit-form="editForm"
@create-one="handleCreate"
@edit-one="handleEdit"
@delete-one="tagStore.actions.deleteOne"
@bulk-action="handleBulkAction"
/>
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { validators } from "~/composables/use-validators";
import { useTagStore, useTagData } from "~/composables/store";
import type { RecipeTag } from "~/lib/api/types/admin";
import { useTagStore } from "~/composables/store";
import { fieldTypes } from "~/composables/forms";
import type { AutoFormItems } from "~/types/auto-forms";
import type { RecipeTag } from "~/lib/api/types/recipe";
import type { TableHeaders, TableConfig } from "~/components/global/CrudTable.vue";
export default defineNuxtComponent({
setup() {
const i18n = useI18n();
const i18n = useI18n();
const tableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
];
const state = reactive({
createDialog: false,
editDialog: false,
deleteDialog: false,
bulkDeleteDialog: false,
});
const tagData = useTagData();
const tagStore = useTagStore();
// ============================================================
// Create Tag
async function createTag() {
await tagStore.actions.createOne({
name: tagData.data.name,
slug: "",
});
tagData.reset();
state.createDialog = false;
}
// ============================================================
// Edit Tag
const editTarget = ref<RecipeTag | null>(null);
function editEventHandler(item: RecipeTag) {
state.editDialog = true;
editTarget.value = item;
}
async function editSaveTag() {
if (!editTarget.value) {
return;
}
await tagStore.actions.updateOne(editTarget.value);
state.editDialog = false;
}
// ============================================================
// Delete Tag
const deleteTarget = ref<RecipeTag | null>(null);
function deleteEventHandler(item: RecipeTag) {
state.deleteDialog = true;
deleteTarget.value = item;
}
async function deleteTag() {
if (!deleteTarget.value || deleteTarget.value.id === undefined) {
return;
}
await tagStore.actions.deleteOne(deleteTarget.value.id!);
state.deleteDialog = false;
}
// ============================================================
// Bulk Delete Tag
const bulkDeleteTarget = ref<RecipeTag[]>([]);
function bulkDeleteEventHandler(selection: RecipeTag[]) {
bulkDeleteTarget.value = selection;
state.bulkDeleteDialog = true;
}
async function deleteSelected() {
const ids = bulkDeleteTarget.value.map(item => item.id).filter(id => !!id);
await tagStore.actions.deleteMany(ids);
bulkDeleteTarget.value = [];
}
return {
state,
tableConfig,
tableHeaders,
tags: tagStore.store,
validators,
// create
createTarget: tagData.data,
createTag,
// edit
editTarget,
editEventHandler,
editSaveTag,
// delete
deleteTarget,
deleteEventHandler,
deleteTag,
// bulk delete
bulkDeleteTarget,
bulkDeleteEventHandler,
deleteSelected,
};
const tableConfig: TableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders: TableHeaders[] = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
];
const tagStore = useTagStore();
// ============================================================
// Form items (shared)
const formItems = [
{
label: i18n.t("general.name"),
varName: "name",
type: fieldTypes.TEXT,
rules: [validators.required],
},
] as AutoFormItems;
// ============================================================
// Create
const createForm = reactive({
items: formItems,
data: { name: "" } as RecipeTag,
});
async function handleCreate(createFormData: RecipeTag) {
await tagStore.actions.createOne(createFormData);
createForm.data.name = "";
}
// ============================================================
// Edit
const editForm = reactive({
items: formItems,
data: {} as RecipeTag,
});
async function handleEdit(editFormData: RecipeTag) {
await tagStore.actions.updateOne(editFormData);
editForm.data = {} as RecipeTag;
}
// ============================================================
// Bulk Actions
async function handleBulkAction(event: string, items: RecipeTag[]) {
if (event === "delete-selected") {
const ids = items.filter(item => item.id != null).map(item => item.id!);
await tagStore.actions.deleteMany(ids);
}
}
</script>

View File

@@ -1,299 +1,133 @@
<template>
<div>
<!-- Create Dialog -->
<BaseDialog
v-model="state.createDialog"
:title="$t('data-pages.tools.new-tool')"
:icon="$globals.icons.potSteam"
can-submit
@submit="createTool"
>
<v-card-text>
<v-form ref="domNewToolForm">
<v-text-field
v-model="createTarget.name"
autofocus
:label="$t('general.name')"
:rules="[validators.required]"
/>
<v-checkbox
v-model="createTarget.onHand"
:label="$t('tool.on-hand')"
/>
</v-form>
</v-card-text>
</BaseDialog>
<!-- Edit Dialog -->
<BaseDialog
v-model="state.editDialog"
:icon="$globals.icons.potSteam"
:title="$t('data-pages.tools.edit-tool')"
:submit-text="$t('general.save')"
can-submit
@submit="editSaveTool"
>
<v-card-text v-if="editTarget">
<div class="mt-4">
<v-text-field
v-model="editTarget.name"
:label="$t('general.name')"
/>
<v-checkbox
v-model="editTarget.onHand"
:label="$t('tool.on-hand')"
hide-details
/>
</div>
</v-card-text>
</BaseDialog>
<!-- Delete Dialog -->
<BaseDialog
v-model="state.deleteDialog"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteTool"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
<p
v-if="deleteTarget"
class="mt-4 ml-4"
>
{{ deleteTarget.name }}
</p>
</v-card-text>
</BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="state.bulkDeleteDialog"
width="650px"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">
{{ $t('general.confirm-delete-generic-items') }}
</p>
<v-card variant="outlined">
<v-virtual-scroll
height="400"
item-height="25"
:items="bulkDeleteTarget"
>
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Data Table -->
<BaseCardSectionTitle
:icon="$globals.icons.potSteam"
section
<GroupDataPage
:icon="$globals.icons.tools"
:title="$t('data-pages.tools.tool-data')"
/>
<CrudTable
v-model:headers="tableHeaders"
:table-headers="tableHeaders"
:table-config="tableConfig"
:data="tools || []"
:bulk-actions="[{ icon: $globals.icons.delete, text: $t('general.delete'), event: 'delete-selected' }]"
initial-sort="name"
@delete-one="deleteEventHandler"
@edit-one="editEventHandler"
@delete-selected="bulkDeleteEventHandler"
:create-form="createForm"
:edit-form="editForm"
@create-one="handleCreate"
@edit-one="handleEdit"
@delete-one="toolStore.actions.deleteOne"
@bulk-action="handleBulkAction"
>
<template #button-row>
<BaseButton
create
@click="state.createDialog = true"
>
{{ $t("general.create") }}
</BaseButton>
</template>
<template #[`item.onHand`]="{ item }">
<v-icon :color="item.onHand ? 'success' : undefined">
{{ item.onHand ? $globals.icons.check : $globals.icons.close }}
</v-icon>
</template>
</CrudTable>
</GroupDataPage>
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import { validators } from "~/composables/use-validators";
import { useToolStore, useToolData } from "~/composables/store";
import type { RecipeTool } from "~/lib/api/types/recipe";
import { fieldTypes } from "~/composables/forms";
import type { AutoFormItems } from "~/types/auto-forms";
import { useToolStore } from "~/composables/store";
import type { RecipeTool, RecipeToolCreate } from "~/lib/api/types/recipe";
import type { TableHeaders, TableConfig } from "~/components/global/CrudTable.vue";
interface RecipeToolWithOnHand extends RecipeTool {
onHand: boolean;
}
export default defineNuxtComponent({
setup() {
const i18n = useI18n();
const auth = useMealieAuth();
const tableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
{
text: i18n.t("tool.on-hand"),
value: "onHand",
show: true,
sortable: true,
},
];
const state = reactive({
createDialog: false,
editDialog: false,
deleteDialog: false,
bulkDeleteDialog: false,
});
const userHousehold = computed(() => auth.user.value?.householdSlug || "");
const toolData = useToolData();
const toolStore = useToolStore();
const tools = computed(() => toolStore.store.value.map((tools) => {
const onHand = tools.householdsWithTool?.includes(userHousehold.value) || false;
return { ...tools, onHand } as RecipeToolWithOnHand;
}));
// ============================================================
// Create Tool
async function createTool() {
if (toolData.data.onHand) {
toolData.data.householdsWithTool = [userHousehold.value];
}
else {
toolData.data.householdsWithTool = [];
}
await toolStore.actions.createOne({
name: toolData.data.name, householdsWithTool: toolData.data.householdsWithTool,
id: "",
slug: "",
});
toolData.reset();
state.createDialog = false;
}
// ============================================================
// Edit Tool
const editTarget = ref<RecipeToolWithOnHand | null>(null);
function editEventHandler(item: RecipeToolWithOnHand) {
state.editDialog = true;
editTarget.value = item;
}
async function editSaveTool() {
if (!editTarget.value) {
return;
}
if (editTarget.value.onHand && !editTarget.value.householdsWithTool?.includes(userHousehold.value)) {
if (!editTarget.value.householdsWithTool) {
editTarget.value.householdsWithTool = [userHousehold.value];
}
else {
editTarget.value.householdsWithTool.push(userHousehold.value);
}
}
else if (!editTarget.value.onHand && editTarget.value.householdsWithTool?.includes(userHousehold.value)) {
editTarget.value.householdsWithTool = editTarget.value.householdsWithTool.filter(
household => household !== userHousehold.value,
);
}
await toolStore.actions.updateOne(editTarget.value);
state.editDialog = false;
}
// ============================================================
// Delete Tool
const deleteTarget = ref<RecipeToolWithOnHand | null>(null);
function deleteEventHandler(item: RecipeToolWithOnHand) {
state.deleteDialog = true;
deleteTarget.value = item;
}
async function deleteTool() {
if (!deleteTarget.value || deleteTarget.value.id === undefined) {
return;
}
await toolStore.actions.deleteOne(deleteTarget.value.id);
state.deleteDialog = false;
}
// ============================================================
// Bulk Delete Tool
const bulkDeleteTarget = ref<RecipeToolWithOnHand[]>([]);
function bulkDeleteEventHandler(selection: RecipeToolWithOnHand[]) {
bulkDeleteTarget.value = selection;
state.bulkDeleteDialog = true;
}
async function deleteSelected() {
const ids = bulkDeleteTarget.value.map(item => item.id);
await toolStore.actions.deleteMany(ids);
bulkDeleteTarget.value = [];
}
return {
state,
tableConfig,
tableHeaders,
tools,
validators,
// create
createTarget: toolData.data,
createTool,
// edit
editTarget,
editEventHandler,
editSaveTool,
// delete
deleteTarget,
deleteEventHandler,
deleteTool,
// bulk delete
bulkDeleteTarget,
bulkDeleteEventHandler,
deleteSelected,
};
const i18n = useI18n();
const tableConfig: TableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders: TableHeaders[] = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
{
text: i18n.t("tool.on-hand"),
value: "onHand",
show: true,
sortable: true,
},
];
const auth = useMealieAuth();
const userHousehold = computed(() => auth.user.value?.householdSlug || "");
const toolStore = useToolStore();
const tools = computed(() => toolStore.store.value.map((tools) => {
const onHand = tools.householdsWithTool?.includes(userHousehold.value) || false;
return { ...tools, onHand } as RecipeToolWithOnHand;
}));
// ============================================================
// Form items (shared)
const formItems = [
{
label: i18n.t("general.name"),
varName: "name",
type: fieldTypes.TEXT,
rules: [validators.required],
},
{
label: i18n.t("tool.on-hand"),
varName: "onHand",
type: fieldTypes.BOOLEAN,
},
] as AutoFormItems;
// ============================================================
// Create
const createForm = reactive({
items: formItems,
data: { name: "", onHand: false } as RecipeToolCreate,
});
async function handleCreate(createFormData: RecipeToolCreate) {
// @ts-expect-error createOne eroniusly expects id and slug which are not preset at time of creation
await toolStore.actions.createOne({ name: createFormData.name, householdsWithTool: createFormData.onHand ? [userHousehold.value] : [] } as RecipeToolCreate);
createForm.data = { name: "", onHand: false } as RecipeToolCreate;
}
// ============================================================
// Edit
const editForm = reactive({
items: formItems,
data: {} as RecipeToolWithOnHand,
});
async function handleEdit(editFormData: RecipeToolWithOnHand) {
// if list of households is undefined default to empty array
if (!editFormData.householdsWithTool) {
editFormData.householdsWithTool = [];
}
if (editFormData.onHand && !editFormData.householdsWithTool.includes(userHousehold.value)) {
editFormData.householdsWithTool.push(userHousehold.value);
}
else if (!editFormData.onHand && editFormData.householdsWithTool.includes(userHousehold.value)) {
const idx = editFormData.householdsWithTool.indexOf(userHousehold.value);
if (idx !== -1) editFormData.householdsWithTool.splice(idx, 1);
}
await toolStore.actions.updateOne({ ...editFormData, id: editFormData.id } as RecipeTool);
editForm.data = {} as RecipeToolWithOnHand;
}
// ============================================================
// Bulk Actions
async function handleBulkAction(event: string, items: RecipeToolWithOnHand[]) {
if (event === "delete-selected") {
const ids = items.filter(item => item.id != null).map(item => item.id!);
await toolStore.actions.deleteMany(ids);
}
}
</script>

View File

@@ -18,15 +18,16 @@
<v-autocomplete
v-model="fromUnit"
return-object
:items="store"
:items="unitStore"
:custom-filter="normalizeFilter"
item-title="name"
:label="$t('data-pages.units.source-unit')"
class="mt-2"
/>
<v-autocomplete
v-model="toUnit"
return-object
:items="store"
:items="unitStore"
:custom-filter="normalizeFilter"
item-title="name"
:label="$t('data-pages.units.target-unit')"
@@ -40,177 +41,16 @@
</v-card-text>
</BaseDialog>
<!-- Create Dialog -->
<BaseDialog
v-model="createDialog"
:icon="$globals.icons.units"
:title="$t('data-pages.units.create-unit')"
:submit-icon="$globals.icons.save"
:submit-text="$t('general.save')"
can-submit
@submit="createUnit"
>
<v-card-text>
<v-form ref="domNewUnitForm">
<v-text-field
v-model="createTarget.name"
autofocus
:label="$t('general.name')"
:hint="$t('data-pages.units.example-unit-singular')"
:rules="[validators.required]"
/>
<v-text-field
v-model="createTarget.pluralName"
:label="$t('general.plural-name')"
:hint="$t('data-pages.units.example-unit-plural')"
/>
<v-text-field
v-model="createTarget.abbreviation"
:label="$t('data-pages.units.abbreviation')"
:hint="$t('data-pages.units.example-unit-abbreviation-singular')"
/>
<v-text-field
v-model="createTarget.pluralAbbreviation"
:label="$t('data-pages.units.plural-abbreviation')"
:hint="$t('data-pages.units.example-unit-abbreviation-plural')"
/>
<v-text-field
v-model="createTarget.description"
:label="$t('data-pages.units.description')"
/>
<v-checkbox
v-model="createTarget.fraction"
hide-details
:label="$t('data-pages.units.display-as-fraction')"
/>
<v-checkbox
v-model="createTarget.useAbbreviation"
hide-details
:label="$t('data-pages.units.use-abbreviation')"
/>
</v-form>
</v-card-text>
</BaseDialog>
<!-- Alias Sub-Dialog -->
<RecipeDataAliasManagerDialog
v-if="editTarget"
v-if="editForm.data"
v-model="aliasManagerDialog"
:data="editTarget"
:data="editForm.data"
can-submit
@submit="updateUnitAlias"
@cancel="aliasManagerDialog = false"
/>
<!-- Edit Dialog -->
<BaseDialog
v-model="editDialog"
:icon="$globals.icons.units"
:title="$t('data-pages.units.edit-unit')"
:submit-icon="$globals.icons.save"
:submit-text="$t('general.save')"
can-submit
@submit="editSaveUnit"
>
<v-card-text v-if="editTarget">
<v-form ref="domEditUnitForm">
<v-text-field
v-model="editTarget.name"
:label="$t('general.name')"
:hint="$t('data-pages.units.example-unit-singular')"
:rules="[validators.required]"
/>
<v-text-field
v-model="editTarget.pluralName"
:label="$t('general.plural-name')"
:hint="$t('data-pages.units.example-unit-plural')"
/>
<v-text-field
v-model="editTarget.abbreviation"
:label="$t('data-pages.units.abbreviation')"
:hint="$t('data-pages.units.example-unit-abbreviation-singular')"
/>
<v-text-field
v-model="editTarget.pluralAbbreviation"
:label="$t('data-pages.units.plural-abbreviation')"
:hint="$t('data-pages.units.example-unit-abbreviation-plural')"
/>
<v-text-field
v-model="editTarget.description"
:label="$t('data-pages.units.description')"
/>
<v-checkbox
v-model="editTarget.fraction"
hide-details
:label="$t('data-pages.units.display-as-fraction')"
/>
<v-checkbox
v-model="editTarget.useAbbreviation"
hide-details
:label="$t('data-pages.units.use-abbreviation')"
/>
</v-form>
</v-card-text>
<template #custom-card-action>
<BaseButton
edit
@click="aliasManagerEventHandler"
>
{{ $t('data-pages.manage-aliases') }}
</BaseButton>
</template>
</BaseDialog>
<!-- Delete Dialog -->
<BaseDialog
v-model="deleteDialog"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteUnit"
>
<v-card-text>
{{ $t("general.confirm-delete-generic") }}
<p
v-if="deleteTarget"
class="mt-4 ml-4"
>
{{ deleteTarget.name }}
</p>
</v-card-text>
</BaseDialog>
<!-- Bulk Delete Dialog -->
<BaseDialog
v-model="bulkDeleteDialog"
width="650px"
:title="$t('general.confirm')"
:icon="$globals.icons.alertCircle"
color="error"
can-confirm
@confirm="deleteSelected"
>
<v-card-text>
<p class="h4">
{{ $t('general.confirm-delete-generic-items') }}
</p>
<v-card variant="outlined">
<v-virtual-scroll
height="400"
item-height="25"
:items="bulkDeleteTarget"
>
<template #default="{ item }">
<v-list-item class="pb-2">
<v-list-item-title>{{ item.name }}</v-list-item-title>
</v-list-item>
</template>
</v-virtual-scroll>
</v-card>
</v-card-text>
</BaseDialog>
<!-- Seed Dialog -->
<BaseDialog
v-model="seedDialog"
@@ -243,7 +83,7 @@
</v-autocomplete>
<v-alert
v-if="store && store.length > 0"
v-if="unitStore && unitStore.length > 0"
type="error"
class="mb-0 text-body-2"
>
@@ -252,63 +92,65 @@
</v-card-text>
</BaseDialog>
<!-- Data Table -->
<BaseCardSectionTitle
<GroupDataPage
:icon="$globals.icons.units"
section
:title="$t('data-pages.units.unit-data')"
/>
<CrudTable
v-model:headers="tableHeaders"
:title="$t('general.units')"
:table-headers="tableHeaders"
:table-config="tableConfig"
:data="store"
:data="unitStore || []"
:bulk-actions="[{ icon: $globals.icons.delete, text: $t('general.delete'), event: 'delete-selected' }]"
initial-sort="createdAt"
initial-sort-desc
@delete-one="deleteEventHandler"
@edit-one="editEventHandler"
@create-one="createEventHandler"
@delete-selected="bulkDeleteEventHandler"
:create-form="createForm"
:edit-form="editForm"
@create-one="handleCreate"
@edit-one="handleEdit"
@delete-one="unitActions.deleteOne"
@bulk-action="handleBulkAction"
>
<template #button-row>
<template #table-button-row>
<BaseButton
create
@click="createDialog = true"
/>
<BaseButton @click="mergeDialog = true">
<template #icon>
{{ $globals.icons.externalLink }}
</template>
:icon="$globals.icons.externalLink"
@click="mergeDialog = true"
>
{{ $t('data-pages.combine') }}
</BaseButton>
</template>
<template #[`item.useAbbreviation`]="{ item }">
<v-icon :color="item.useAbbreviation ? 'success' : undefined">
{{ item.useAbbreviation ? $globals.icons.check : $globals.icons.close }}
</v-icon>
</template>
<template #[`item.fraction`]="{ item }">
<v-icon :color="item.fraction ? 'success' : undefined">
{{ item.fraction ? $globals.icons.check : $globals.icons.close }}
</v-icon>
</template>
<template #[`item.createdAt`]="{ item }">
{{ item.createdAt ? $d(new Date(item.createdAt)) : '' }}
</template>
<template #button-bottom>
<BaseButton @click="seedDialog = true">
<template #icon>
{{ $globals.icons.database }}
</template>
<template #table-button-bottom>
<BaseButton :icon="$globals.icons.database" @click="seedDialog = true">
{{ $t('data-pages.seed') }}
</BaseButton>
</template>
</CrudTable>
<template #edit-dialog-custom-action>
<BaseButton
:icon="$globals.icons.tags"
color="info"
@click="aliasManagerDialog = true"
>
{{ $t('data-pages.manage-aliases') }}
</BaseButton>
</template>
</GroupDataPage>
</div>
</template>
<script lang="ts">
<script setup lang="ts">
import type { LocaleObject } from "@nuxtjs/i18n";
import RecipeDataAliasManagerDialog from "~/components/Domain/Recipe/RecipeDataAliasManagerDialog.vue";
import { validators } from "~/composables/use-validators";
@@ -317,265 +159,213 @@ import type { CreateIngredientUnit, IngredientUnit, IngredientUnitAlias } from "
import { useLocales } from "~/composables/use-locales";
import { normalizeFilter } from "~/composables/use-utils";
import { useUnitStore } from "~/composables/store";
import type { VForm } from "~/types/auto-forms";
import type { AutoFormItems } from "~/types/auto-forms";
import type { TableHeaders, TableConfig } from "~/components/global/CrudTable.vue";
import { fieldTypes } from "~/composables/forms";
export default defineNuxtComponent({
components: { RecipeDataAliasManagerDialog },
setup() {
const userApi = useUserApi();
const i18n = useI18n();
const userApi = useUserApi();
const i18n = useI18n();
const tableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
{
text: i18n.t("general.plural-name"),
value: "pluralName",
show: true,
sortable: true,
},
{
text: i18n.t("data-pages.units.abbreviation"),
value: "abbreviation",
show: true,
sortable: true,
},
{
text: i18n.t("data-pages.units.plural-abbreviation"),
value: "pluralAbbreviation",
show: true,
sortable: true,
},
{
text: i18n.t("data-pages.units.use-abbv"),
value: "useAbbreviation",
show: true,
sortable: true,
},
{
text: i18n.t("data-pages.units.description"),
value: "description",
show: false,
},
{
text: i18n.t("data-pages.units.fraction"),
value: "fraction",
show: true,
sortable: true,
},
{
text: i18n.t("general.date-added"),
value: "createdAt",
show: false,
sortable: true,
},
];
const { store, actions: unitActions } = useUnitStore();
// ============================================================
// Create Units
const createDialog = ref(false);
const domNewUnitForm = ref<VForm>();
// we explicitly set booleans to false since forms don't POST unchecked boxes
const createTarget = ref<CreateIngredientUnit>({
name: "",
fraction: true,
useAbbreviation: false,
});
function createEventHandler() {
createDialog.value = true;
}
async function createUnit() {
if (!createTarget.value || !createTarget.value.name) {
return;
}
// @ts-expect-error the createOne function erroneously expects an id because it uses the IngredientUnit type
await unitActions.createOne(createTarget.value);
createDialog.value = false;
domNewUnitForm.value?.reset();
createTarget.value = {
name: "",
fraction: false,
useAbbreviation: false,
};
}
// ============================================================
// Edit Units
const editDialog = ref(false);
const editTarget = ref<IngredientUnit | null>(null);
function editEventHandler(item: IngredientUnit) {
editTarget.value = item;
editDialog.value = true;
}
async function editSaveUnit() {
if (!editTarget.value) {
return;
}
await unitActions.updateOne(editTarget.value);
editDialog.value = false;
}
// ============================================================
// Delete Units
const deleteDialog = ref(false);
const deleteTarget = ref<IngredientUnit | null>(null);
function deleteEventHandler(item: IngredientUnit) {
deleteTarget.value = item;
deleteDialog.value = true;
}
async function deleteUnit() {
if (!deleteTarget.value) {
return;
}
await unitActions.deleteOne(deleteTarget.value.id);
deleteDialog.value = false;
}
// ============================================================
// Bulk Delete Units
const bulkDeleteDialog = ref(false);
const bulkDeleteTarget = ref<IngredientUnit[]>([]);
function bulkDeleteEventHandler(selection: IngredientUnit[]) {
bulkDeleteTarget.value = selection;
bulkDeleteDialog.value = true;
}
async function deleteSelected() {
const ids = bulkDeleteTarget.value.map(item => item.id);
await unitActions.deleteMany(ids);
bulkDeleteTarget.value = [];
}
// ============================================================
// Alias Manager
const aliasManagerDialog = ref(false);
function aliasManagerEventHandler() {
aliasManagerDialog.value = true;
}
function updateUnitAlias(newAliases: IngredientUnitAlias[]) {
if (!editTarget.value) {
return;
}
editTarget.value.aliases = newAliases;
aliasManagerDialog.value = false;
}
// ============================================================
// Merge Units
const mergeDialog = ref(false);
const fromUnit = ref<IngredientUnit | null>(null);
const toUnit = ref<IngredientUnit | null>(null);
const canMerge = computed(() => {
return fromUnit.value && toUnit.value && fromUnit.value.id !== toUnit.value.id;
});
async function mergeUnits() {
if (!canMerge.value || !fromUnit.value || !toUnit.value) {
return;
}
const { data } = await userApi.units.merge(fromUnit.value.id, toUnit.value.id);
if (data) {
unitActions.refresh();
}
}
// ============================================================
// Seed
const seedDialog = ref(false);
const locale = ref("");
const { locales: LOCALES, locale: currentLocale } = useLocales();
onMounted(() => {
locale.value = currentLocale.value;
});
const locales = LOCALES.filter(locale =>
(i18n.locales.value as LocaleObject[]).map(i18nLocale => i18nLocale.code).includes(locale.value),
);
async function seedDatabase() {
const { data } = await userApi.seeders.units({ locale: locale.value });
if (data) {
unitActions.refresh();
}
}
return {
tableConfig,
tableHeaders,
store,
validators,
normalizeFilter,
// Create
createDialog,
domNewUnitForm,
createEventHandler,
createUnit,
createTarget,
// Edit
editDialog,
editEventHandler,
editSaveUnit,
editTarget,
// Delete
deleteEventHandler,
deleteDialog,
deleteUnit,
deleteTarget,
// Bulk Delete
bulkDeleteDialog,
bulkDeleteEventHandler,
bulkDeleteTarget,
deleteSelected,
// Alias Manager
aliasManagerDialog,
aliasManagerEventHandler,
updateUnitAlias,
// Merge
canMerge,
mergeUnits,
mergeDialog,
fromUnit,
toUnit,
// Seed
seedDatabase,
locales,
locale,
seedDialog,
};
const tableConfig: TableConfig = {
hideColumns: true,
canExport: true,
};
const tableHeaders: TableHeaders[] = [
{
text: i18n.t("general.id"),
value: "id",
show: false,
},
{
text: i18n.t("general.name"),
value: "name",
show: true,
sortable: true,
},
{
text: i18n.t("general.plural-name"),
value: "pluralName",
show: true,
sortable: true,
},
{
text: i18n.t("data-pages.units.abbreviation"),
value: "abbreviation",
show: true,
sortable: true,
},
{
text: i18n.t("data-pages.units.plural-abbreviation"),
value: "pluralAbbreviation",
show: true,
sortable: true,
},
{
text: i18n.t("data-pages.units.use-abbv"),
value: "useAbbreviation",
show: true,
sortable: true,
},
{
text: i18n.t("data-pages.units.description"),
value: "description",
show: false,
},
{
text: i18n.t("data-pages.units.fraction"),
value: "fraction",
show: true,
sortable: true,
},
{
text: i18n.t("general.date-added"),
value: "createdAt",
show: false,
sortable: true,
},
];
const { store: unitStore, actions: unitActions } = useUnitStore();
// ============================================================
// Form items (shared)
const formItems: AutoFormItems = [
{
label: i18n.t("general.name"),
varName: "name",
type: fieldTypes.TEXT,
rules: [validators.required],
},
{
label: i18n.t("general.plural-name"),
varName: "pluralName",
type: fieldTypes.TEXT,
},
{
label: i18n.t("data-pages.units.abbreviation"),
varName: "abbreviation",
type: fieldTypes.TEXT,
},
{
label: i18n.t("data-pages.units.plural-abbreviation"),
varName: "pluralAbbreviation",
type: fieldTypes.TEXT,
},
{
label: i18n.t("data-pages.units.description"),
varName: "description",
type: fieldTypes.TEXT,
},
{
label: i18n.t("data-pages.units.use-abbv"),
varName: "useAbbreviation",
type: fieldTypes.BOOLEAN,
},
{
label: i18n.t("data-pages.units.fraction"),
varName: "fraction",
type: fieldTypes.BOOLEAN,
},
];
// ============================================================
// Create
const createForm = reactive({
items: formItems,
data: {
name: "",
fraction: false,
useAbbreviation: false,
} as CreateIngredientUnit,
});
async function handleCreate(createFormData: CreateIngredientUnit) {
// @ts-expect-error createOne eroniusly expects id which is not preset at time of creation
await unitActions.createOne(createFormData);
createForm.data = {
name: "",
fraction: false,
useAbbreviation: false,
} as CreateIngredientUnit;
}
// ============================================================
// Edit
const editForm = reactive({
items: formItems,
data: {} as IngredientUnit,
});
async function handleEdit(editFormData: IngredientUnit) {
await unitActions.updateOne(editFormData);
editForm.data = {} as IngredientUnit;
}
// ============================================================
// Bulk Actions
async function handleBulkAction(event: string, items: IngredientUnit[]) {
if (event === "delete-selected") {
const ids = items.filter(item => item.id != null).map(item => item.id!);
await unitActions.deleteMany(ids);
}
}
// ============================================================
// Alias Manager
const aliasManagerDialog = ref(false);
function updateUnitAlias(newAliases: IngredientUnitAlias[]) {
if (!editForm.data) {
return;
}
editForm.data.aliases = newAliases;
aliasManagerDialog.value = false;
}
// ============================================================
// Merge Units
const mergeDialog = ref(false);
const fromUnit = ref<IngredientUnit | null>(null);
const toUnit = ref<IngredientUnit | null>(null);
const canMerge = computed(() => {
return fromUnit.value && toUnit.value && fromUnit.value.id !== toUnit.value.id;
});
async function mergeUnits() {
if (!canMerge.value || !fromUnit.value || !toUnit.value) {
return;
}
const { data } = await userApi.units.merge(fromUnit.value.id, toUnit.value.id);
if (data) {
unitActions.refresh();
}
}
// ============================================================
// Seed
const seedDialog = ref(false);
const locale = ref("");
const { locales: LOCALES, locale: currentLocale } = useLocales();
onMounted(() => {
locale.value = currentLocale.value;
});
const locales = LOCALES.filter(locale =>
(i18n.locales.value as LocaleObject[]).map(i18nLocale => i18nLocale.code).includes(locale.value as any),
);
async function seedDatabase() {
const { data } = await userApi.seeders.units({ locale: locale.value });
if (data) {
unitActions.refresh();
}
}
</script>