Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 110 additions & 97 deletions admin/src/routes/menus/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,26 @@

const pageTitle = 'Menü Yönetimi | Admin Panel';

// Backend Menu model: id, name, link, subMenu, icon, active, order, categories[], createdAt, updatedAt
interface MenuCategory {
id: number;
name: string;
}
interface Menu {
id: string;
id: number;
name: string;
slug: string;
url: string;
link: string;
subMenu: number;
icon?: string | null;
active: boolean;
order: number;
isActive: boolean;
parentId?: string;
categoryId?: string;
categories?: MenuCategory[];
createdAt: string;
updatedAt: string;
}

let menus: Menu[] = [];
let menuCategories: MenuCategory[] = [];
let loading = false;
let isModalOpen = false;
let isDeleteModalOpen = false;
Expand All @@ -35,54 +41,63 @@
let totalItems = 0;
const itemsPerPage = 10;

// Form data
// Form data (backend field names)
let formData = {
name: '',
slug: '',
url: '',
link: '',
order: 0,
isActive: true,
parentId: '',
categoryId: ''
active: true,
subMenu: 0,
icon: '',
categoryIds: [] as number[]
};

const columns = [
{ key: 'name', label: 'Menü Adı' },
{ key: 'slug', label: 'Slug' },
{ key: 'url', label: 'URL' },
{ key: 'link', label: 'URL' },
{ key: 'order', label: 'Sıra' },
{ key: 'isActive', label: 'Aktif' },
{ key: 'active', label: 'Aktif' },
{ key: 'createdAt', label: 'Oluşturulma' }
];

onMount(() => {
loadMenus();
loadMenuCategories();
});

async function loadMenus() {
loading = true;
try {
const response = await getData<Menu>('menus', false, itemsPerPage, currentPage);
menus = response.data || [];
totalItems = (response.meta as any)?.total || menus.length;
totalPages = Math.ceil(totalItems / itemsPerPage);
totalItems = (response.meta as any)?.total ?? menus.length;
totalPages = Math.ceil(totalItems / itemsPerPage) || 1;
} catch (error) {
toastStore.add('error', 'Menüler yüklenirken hata oluştu');
} finally {
loading = false;
}
}

async function loadMenuCategories() {
try {
const response = await getData<MenuCategory>('menu-categories', false, 100, 1);
menuCategories = response.data || [];
} catch {
menuCategories = [];
}
}

function openAddModal() {
editingMenu = null;
formData = {
name: '',
slug: '',
url: '',
link: '',
order: 0,
isActive: true,
parentId: '',
categoryId: ''
active: true,
subMenu: 0,
icon: '',
categoryIds: []
};
isModalOpen = true;
}
Expand All @@ -91,12 +106,12 @@
editingMenu = menu;
formData = {
name: menu.name,
slug: menu.slug,
url: menu.url,
link: menu.link,
order: menu.order,
isActive: menu.isActive,
parentId: menu.parentId || '',
categoryId: menu.categoryId || ''
active: menu.active,
subMenu: menu.subMenu ?? 0,
icon: menu.icon || '',
categoryIds: menu.categories?.map((c) => c.id) ?? []
};
isModalOpen = true;
}
Expand All @@ -116,18 +131,32 @@
deletingMenu = null;
}

function toggleCategory(id: number) {
const idx = formData.categoryIds.indexOf(id);
if (idx === -1) {
formData.categoryIds = [...formData.categoryIds, id];
} else {
formData.categoryIds = formData.categoryIds.filter((x) => x !== id);
}
}

async function handleSubmit(e: Event) {
e.preventDefault();
loading = true;

try {
const data: any = { ...formData };
// Remove empty optional fields
if (!data.parentId) delete data.parentId;
if (!data.categoryId) delete data.categoryId;
const payload = {
name: formData.name.trim(),
link: formData.link.trim(),
order: Number(formData.order) || 0,
active: formData.active,
subMenu: Number(formData.subMenu) || 0,
icon: formData.icon.trim() || null,
categoryIds: formData.categoryIds
};

if (editingMenu) {
const result = await updateData('menus', editingMenu.id, data);
const result = await updateData('menus', editingMenu.id, payload);
if (result.success) {
toastStore.add('success', 'Menü başarıyla güncellendi');
closeModal();
Expand All @@ -136,7 +165,7 @@
toastStore.add('error', result.message || 'Güncelleme başarısız');
}
} else {
const result = await addData('menus', data);
const result = await addData('menus', payload);
if (result.success) {
toastStore.add('success', 'Menü başarıyla eklendi');
closeModal();
Expand Down Expand Up @@ -176,21 +205,6 @@
currentPage = page;
loadMenus();
}

function generateSlug() {
if (formData.name) {
formData.slug = formData.name
.toLowerCase()
.replace(/ğ/g, 'g')
.replace(/ü/g, 'u')
.replace(/ş/g, 's')
.replace(/ı/g, 'i')
.replace(/ö/g, 'o')
.replace(/ç/g, 'c')
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
}
}
</script>

<svelte:head>
Expand Down Expand Up @@ -245,32 +259,15 @@
bind:value={formData.name}
required
placeholder="Ana Sayfa, Hakkımızda, vb."
on:blur={generateSlug}
/>

<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
type="text"
name="slug"
label="Slug"
bind:value={formData.slug}
required
placeholder="ana-sayfa"
/>
<div class="flex items-end">
<Button type="button" variant="secondary" on:click={generateSlug} fullWidth>
Slug Oluştur
</Button>
</div>
</div>

<Input
type="text"
name="url"
name="link"
label="URL"
bind:value={formData.url}
bind:value={formData.link}
required
placeholder="/home, /about, https://example.com"
placeholder="/, /etiketler, /kariyer"
/>

<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
Expand All @@ -279,42 +276,58 @@
name="order"
label="Sıra"
bind:value={formData.order}
required
placeholder="0"
/>
<div class="flex items-end">
<Input
type="checkbox"
name="isActive"
label=""
bind:value={formData.isActive}
placeholder="Menüyü aktif et"
/>
<div class="flex items-end pb-1">
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-gray-300">
<input
type="checkbox"
bind:checked={formData.active}
class="rounded border-gray-300 dark:border-DarkLightGrey"
/>
Aktif
</label>
</div>
</div>

<div class="bg-gray-50 dark:bg-DarkLightGrey p-4 rounded-md">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
İleri Düzey Ayarlar (Opsiyonel)
</h4>
<div class="space-y-4">
<Input
type="text"
name="parentId"
label="Üst Menü ID"
bind:value={formData.parentId}
placeholder="Alt menü için üst menünün ID'si"
/>
<Input
type="text"
name="categoryId"
label="Kategori ID"
bind:value={formData.categoryId}
placeholder="İlişkili kategori ID"
/>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
type="number"
name="subMenu"
label="Üst Menü ID (0 = ana menü)"
bind:value={formData.subMenu}
placeholder="0"
/>
<Input
type="text"
name="icon"
label="İkon (opsiyonel)"
bind:value={formData.icon}
placeholder="AiOutlineHome"
/>
</div>

{#if menuCategories.length > 0}
<div class="bg-gray-50 dark:bg-DarkLightGrey p-4 rounded-md">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">Kategoriler</h4>
<div class="flex flex-wrap gap-2">
{#each menuCategories as cat}
<label
class="inline-flex items-center gap-2 px-3 py-1.5 rounded border border-gray-300 dark:border-DarkLightGrey cursor-pointer"
>
<input
type="checkbox"
checked={formData.categoryIds.includes(cat.id)}
on:change={() => toggleCategory(cat.id)}
class="rounded"
/>
<span class="text-sm">{cat.name}</span>
</label>
{/each}
</div>
</div>
{/if}

<div
class="flex justify-end space-x-3 pt-4 border-t border-gray-200 dark:border-DarkLightGrey"
>
Expand Down
38 changes: 26 additions & 12 deletions backend/src/services/menu.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,7 @@ class MenuService {
}
} : true,
},
orderBy: {
// order by id in ascending order
id: 'asc',
},
orderBy: [{ order: "asc" }, { id: "asc" }],
});

return { menus, count };
Expand Down Expand Up @@ -52,12 +49,20 @@ class MenuService {

public async createMenu(menuData: any): Promise<Menu> {
try {
const categoryIds = Array.isArray(menuData.categoryIds)
? menuData.categoryIds.filter((id: number) => Number.isInteger(id))
: [];
const menu = await this.prisma.menu.create({
data: {
name: menuData.name,
link: menuData.link,
subMenu: menuData.subMenu || 0,
icon: menuData.icon,
subMenu: menuData.subMenu ?? 0,
icon: menuData.icon ?? null,
active: menuData.active !== false,
order: typeof menuData.order === "number" ? menuData.order : 0,
...(categoryIds.length > 0 && {
categories: { connect: categoryIds.map((id: number) => ({ id })) },
}),
},
});

Expand All @@ -69,14 +74,23 @@ class MenuService {

public async updateMenu(id: number, menuData: any): Promise<Menu> {
try {
const categoryIds = Array.isArray(menuData.categoryIds)
? menuData.categoryIds.filter((id: number) => Number.isInteger(id))
: undefined;
const data: any = {
name: menuData.name,
link: menuData.link,
subMenu: menuData.subMenu ?? 0,
icon: menuData.icon ?? null,
active: menuData.active !== false,
order: typeof menuData.order === "number" ? menuData.order : 0,
};
if (categoryIds !== undefined) {
data.categories = { set: categoryIds.map((id: number) => ({ id })) };
}
const menu = await this.prisma.menu.update({
where: { id },
data: {
name: menuData.name,
link: menuData.link,
subMenu: menuData.subMenu,
icon: menuData.icon,
},
data,
});

return menu;
Expand Down
Loading
Loading