Skip to content

Instantly share code, notes, and snippets.

@bulentsakarya
Created March 13, 2026 18:40
Show Gist options
  • Select an option

  • Save bulentsakarya/888ba82d6c706a291a602a3bb7c6918f to your computer and use it in GitHub Desktop.

Select an option

Save bulentsakarya/888ba82d6c706a291a602a3bb7c6918f to your computer and use it in GitHub Desktop.
City Controller index metod
/**
* İl listeleme sayfası.
*/
public function index(?string $countryId = null): Response
{
return Inertia::render('panel/definitions/City/Index', [
'cities' => $this->cityService->all($countryId),
'selectedCountry' => Country::find($countryId),
'selectedCountryId' => $countryId,
]);
}
City Service all metod
public function all(?string $countryId = null): LengthAwarePaginator
{
return City::query()
->with('country:id,name')
->when($countryId, fn ($q) => $q->where('country_id', $countryId))
->orderBy('sort_order')
->orderBy('name')
->paginate(config('otomasyon.pagination.per_page', 15));
}
<script setup lang="ts">
import { Head, router, useForm } from '@inertiajs/vue3';
import {
Plus,
Pencil,
Trash2,
Loader2,
X,
Check,
ChevronLeft,
ChevronRight,
MapPin,
} from 'lucide-vue-next';
import { ref } from 'vue';
import {
store,
update,
destroy,
} from '@/actions/App/Http/Controllers/Panel/Definitions/CityController';
import Heading from '@/components/common/Heading.vue';
import InputError from '@/components/common/InputError.vue';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import EmptyState from '@/components/ui/empty-state/EmptyState.vue';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from '@/components/ui/pagination';
import {
Sheet,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
} from '@/components/ui/sheet';
import { Switch } from '@/components/ui/switch';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { useClearFormErrors } from '@/composables/useClearFormErrors';
import AppLayout from '@/layouts/PanelLayout.vue';
import DefinitionsLayout from '@/pages/panel/definitions/partials/Layout.vue';
import { index as cityRoute } from '@/routes/panel/settings/definitions/cities';
import { index as districtRoute } from '@/routes/panel/settings/definitions/districts';
import {
type BreadcrumbItem,
type City,
type Country,
type PaginationResponse,
} from '@/types';
const props = defineProps<{
cities: PaginationResponse<City>;
selectedCountry?: Country;
selectedCountryId?: string;
}>();
const breadcrumbItems: BreadcrumbItem[] = [
{ title: 'Tanımlamalar', href: '#' },
{ title: 'Şehirler', href: cityRoute().url },
];
const isSheetOpen = ref(false);
const showDeleteDialog = ref(false);
const editingCity = ref<City | null>(null);
const form = useForm({
country_id: props.selectedCountryId || '',
name: '',
is_active: true,
});
useClearFormErrors(form);
function openCreateSheet() {
editingCity.value = null;
form.reset();
form.country_id = props.selectedCountryId || '';
form.clearErrors();
isSheetOpen.value = true;
}
function openEditSheet(city: City) {
editingCity.value = city;
form.country_id = city.country_id;
form.name = city.name;
form.is_active = city.is_active;
form.clearErrors();
isSheetOpen.value = true;
}
function submitForm() {
if (editingCity.value) {
form.put(update.url(editingCity.value.id), {
onSuccess: () => {
isSheetOpen.value = false;
},
});
} else {
form.post(store.url(), {
onSuccess: () => {
isSheetOpen.value = false;
form.reset();
},
});
}
}
function openDeleteDialog() {
showDeleteDialog.value = true;
}
function confirmDelete() {
if (!editingCity.value) return;
form.delete(destroy.url(editingCity.value.id), {
onSuccess: () => {
showDeleteDialog.value = false;
isSheetOpen.value = false;
editingCity.value = null;
},
});
}
function handlePageChange(page: number) {
router.visit(window.location.href, {
data: { page },
preserveScroll: true,
preserveState: true,
});
}
const goToDistricts = (cityId: string) => {
router.get(districtRoute().url, { city: cityId });
};
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbItems">
<Head title="Şehirler" />
<DefinitionsLayout>
<div class="space-y-6">
<div
class="flex flex-col justify-between gap-4 px-2 sm:flex-row sm:items-center"
>
<Heading
variant="small"
title="Şehirler"
description="Sistem üzerinde kullanılan il/şehir tanımlamaları."
/>
<Button size="sm" class="h-9" @click="openCreateSheet">
<Plus class="mr-2 h-4 w-4" />
Yeni Şehir Ekle
</Button>
</div>
<template v-if="cities.data.length > 0">
<div
class="mx-2 rounded-md border border-border bg-card shadow-none"
>
<Table>
<TableHeader>
<TableRow>
<TableHead>Ad</TableHead>
<TableHead class="text-center"
>İlçeler</TableHead
>
<TableHead class="text-center"
>Durum</TableHead
>
<TableHead class="w-[120px] text-right"
>İşlemler</TableHead
>
</TableRow>
</TableHeader>
<TableBody>
<TableRow
v-for="city in cities.data"
:key="city.id"
class="transition-colors hover:bg-muted/50"
>
<TableCell class="font-medium">{{
city.name
}}</TableCell>
<TableCell class="text-center">
<Button
variant="outline"
size="sm"
class="h-8 px-2"
@click="goToDistricts(city.id)"
>
<MapPin class="mr-1 h-3.5 w-3.5" />
İlçeleri Gör
</Button>
</TableCell>
<TableCell class="text-center">
<Badge
:variant="
city.is_active
? 'default'
: 'secondary'
"
>
{{
city.is_active
? 'Aktif'
: 'Pasif'
}}
</Badge>
</TableCell>
<TableCell class="text-right">
<Button
variant="outline"
size="sm"
class="h-8 px-2"
@click="openEditSheet(city)"
>
<Pencil class="mr-1.5 h-3 w-3" />
Düzenle
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<div
v-if="cities.last_page > 1"
class="mt-4 flex flex-col items-center justify-between gap-4 px-2 sm:flex-row"
>
<div class="text-sm font-medium text-muted-foreground">
Toplam {{ cities.total }} kayıttan
{{ cities.from }}-{{ cities.to }} arası gösteriliyor
</div>
<Pagination
:total="cities.total"
:items-per-page="cities.per_page"
:default-page="cities.current_page"
@update:page="handlePageChange"
>
<PaginationContent>
<PaginationPrevious class="cursor-pointer">
<ChevronLeft class="h-4 w-4" />
</PaginationPrevious>
<template
v-for="(item, index) in cities.links"
:key="index"
>
<PaginationItem
v-if="
item.url &&
!isNaN(Number(item.label))
"
:value="index"
>
<Button
size="icon"
variant="ghost"
class="h-9 w-9"
:class="{ border: item.active }"
@click="
handlePageChange(
Number(item.label),
)
"
>
{{ item.label }}
</Button>
</PaginationItem>
</template>
<PaginationNext class="cursor-pointer">
<ChevronRight class="h-4 w-4" />
</PaginationNext>
</PaginationContent>
</Pagination>
</div>
</template>
<div v-else class="mx-2">
<EmptyState
title="Şehir Bulunamadı"
description="Sistemde henüz hiç il oluşturulmamış."
:action-label="'Yeni Şehir Ekle'"
@action="openCreateSheet()"
/>
</div>
</div>
<Sheet v-model:open="isSheetOpen">
<SheetContent
side="right"
class="flex h-full flex-col p-0 sm:max-w-[400px]"
>
<SheetHeader class="shrink-0 border-b p-6">
<SheetTitle class="flex items-center gap-2 text-xl">
<Pencil
v-if="editingCity"
class="h-5 w-5 text-primary"
/>
<Plus v-else class="h-5 w-5 text-primary" />
{{
editingCity
? 'Şehir Düzenle'
: 'Yeni Şehir Ekle'
}}
</SheetTitle>
<SheetDescription>
{{
editingCity
? 'Şehir bilgilerini güncelleyin.'
: 'Sisteme yeni bir il ekleyin.'
}}
</SheetDescription>
</SheetHeader>
<form
id="cityForm"
class="flex-1 space-y-6 overflow-y-auto p-6"
@submit.prevent="submitForm"
>
<div class="space-y-2">
<Label for="name" class="text-sm font-bold"
>Şehir Adı</Label
>
<Input
id="name"
v-model="form.name"
placeholder="İstanbul"
class="h-11"
/>
<InputError :message="form.errors.name" />
</div>
<div class="flex items-center gap-2">
<Switch
id="is_active"
:checked="form.is_active"
@update:checked="form.is_active = $event"
/>
<Label for="is_active">Aktif</Label>
</div>
</form>
<SheetFooter class="shrink-0 border-t bg-muted/10 p-6">
<div class="flex w-full flex-col gap-3">
<Button
type="submit"
form="cityForm"
class="h-11 w-full"
:disabled="form.processing"
>
<Loader2
v-if="form.processing"
class="mr-2 h-4 w-4 animate-spin"
/>
<Check v-else class="mr-2 h-4 w-4" />
{{
form.processing
? 'Kaydediliyor...'
: editingCity
? 'Değişiklikleri Kaydet'
: 'Şehir Ekle'
}}
</Button>
<Button
type="button"
variant="ghost"
class="h-11 w-full"
@click="isSheetOpen = false"
>
<X class="mr-2 h-4 w-4" /> İptal
</Button>
<Button
v-if="editingCity"
type="button"
variant="outline"
class="h-11 w-full border-destructive/30 text-destructive hover:bg-destructive/5"
@click="openDeleteDialog"
>
<Trash2 class="mr-2 h-4 w-4" />
Şehiri Sil
</Button>
</div>
</SheetFooter>
</SheetContent>
</Sheet>
<AlertDialog v-model:open="showDeleteDialog">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle
>Şehiri silmek istediğinize emin
misiniz?</AlertDialogTitle
>
<AlertDialogDescription>
<strong>{{ editingCity?.name }}</strong> kalıcı
olarak silinecektir. Bu işlem geri alınamaz.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>İptal</AlertDialogCancel>
<AlertDialogAction
class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
@click="confirmDelete"
>
Evet, Sil
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</DefinitionsLayout>
</AppLayout>
</template>
Country Controller index metod
/**
* Ülke listeleme sayfası.
*/
public function index(): Response
{
return Inertia::render('panel/definitions/Country/Index', [
'countries' => $this->countryService->all(),
'defaultCountryId' => settings('default_country'),
]);
}
Country Service all metod
public function all(): LengthAwarePaginator
{
return Country::query()
->orderBy('sort_order')
->orderBy('name')
->paginate(config('otomasyon.pagination.per_page', 15));
}
<script setup lang="ts">
import { Head, router, useForm } from '@inertiajs/vue3';
import { Plus, Pencil, Trash2, Loader2, X, Check, ChevronLeft, ChevronRight, MapPin } from 'lucide-vue-next';
import { ref } from 'vue';
import { store, update, destroy } from '@/actions/App/Http/Controllers/Panel/Definitions/CountryController';
import Heading from '@/components/common/Heading.vue';
import InputError from '@/components/common/InputError.vue';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui/alert-dialog';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import EmptyState from '@/components/ui/empty-state/EmptyState.vue';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationNext,
PaginationPrevious,
} from '@/components/ui/pagination';
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Switch } from '@/components/ui/switch';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useClearFormErrors } from '@/composables/useClearFormErrors';
import AppLayout from '@/layouts/PanelLayout.vue';
import DefinitionsLayout from '@/pages/panel/definitions/partials/Layout.vue';
import { index as cityIndex } from '@/routes/panel/settings/definitions/cities';
import { index as countryRoute } from '@/routes/panel/settings/definitions/countries';
import { type BreadcrumbItem, type Country, type PaginationResponse } from '@/types';
const props = defineProps<{
countries: PaginationResponse<Country>;
defaultCountryId: string | null;
}>();
const breadcrumbItems: BreadcrumbItem[] = [
{ title: 'Tanımlamalar', href: '#' },
{ title: 'Ülkeler', href: countryRoute().url },
];
const isSheetOpen = ref(false);
const showDeleteDialog = ref(false);
const editingCountry = ref<Country | null>(null);
const form = useForm({
code: '',
name: '',
is_active: true,
sort_order: 0,
});
useClearFormErrors(form);
function openCreateSheet() {
editingCountry.value = null;
form.reset();
form.clearErrors();
isSheetOpen.value = true;
}
function openEditSheet(country: Country) {
editingCountry.value = country;
form.code = country.code;
form.name = country.name;
form.is_active = country.is_active;
form.sort_order = country.sort_order;
form.clearErrors();
isSheetOpen.value = true;
}
function submitForm() {
if (editingCountry.value) {
form.put(update.url(editingCountry.value.id), {
onSuccess: () => { isSheetOpen.value = false; },
});
} else {
form.post(store.url(), {
onSuccess: () => { isSheetOpen.value = false; form.reset(); },
});
}
}
function openDeleteDialog() {
showDeleteDialog.value = true;
}
function confirmDelete() {
if (!editingCountry.value) return;
form.delete(destroy.url(editingCountry.value.id), {
onSuccess: () => {
showDeleteDialog.value = false;
isSheetOpen.value = false;
editingCountry.value = null;
},
});
}
function handlePageChange(page: number) {
router.visit(countryRoute().url, {
data: { page },
preserveScroll: true,
preserveState: true,
});
}
const goToCities = (countryId: string) => {
router.get(cityIndex().url, { country: countryId });
};
</script>
<template>
<AppLayout :breadcrumbs="breadcrumbItems">
<Head title="Ülkeler" />
<DefinitionsLayout>
<div class="space-y-6">
<div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-center px-2">
<Heading variant="small" title="Ülkeler"
description="Sistem üzerinde kullanılan ülke tanımlamaları." />
<div>
<Button size="sm" class="h-9" @click="openCreateSheet">
<Plus class="mr-2 h-4 w-4" />
Yeni Ülke Ekle
</Button>
</div>
</div>
<template v-if="countries.data.length > 0">
<div class="rounded-md border border-border bg-card shadow-none mx-2">
<Table>
<TableHeader>
<TableRow>
<TableHead>Kod</TableHead>
<TableHead>Ad</TableHead>
<TableHead>Şehirler</TableHead>
<TableHead class="text-center">Varsayılan</TableHead>
<TableHead class="text-center">Durum</TableHead>
<TableHead class="text-right w-[100px]">İşlemler
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="country in countries.data" :key="country.id"
class="hover:bg-muted/50 transition-colors">
<TableCell class="font-medium">{{ country.code }}</TableCell>
<TableCell>{{ country.name }}</TableCell>
<TableCell>
<Button
variant="outline"
size="sm"
class="h-8"
@click="goToCities(country.id)"
>
<MapPin class="mr-1 h-4 w-4" />
İlleri Gör
</Button>
</TableCell>
<TableCell class="text-center">
<Badge v-if="country.id === props.defaultCountryId" variant="default">Varsayılan
</Badge>
</TableCell>
<TableCell class="text-center">
<Badge :variant="country.is_active ? 'default' : 'secondary'">
{{ country.is_active ? 'Aktif' : 'Pasif' }}
</Badge>
</TableCell>
<TableCell class="text-right">
<Button variant="outline" size="sm" class="h-8" @click="openEditSheet(country)">
<Pencil class="mr-2 h-3.5 w-3.5" />
Düzenle
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
</div>
<div v-if="countries.last_page > 1"
class="mt-4 flex flex-col items-center justify-between gap-4 sm:flex-row px-2">
<div class="text-sm text-muted-foreground font-medium">
Toplam {{ countries.total }} kayıttan {{ countries.from }}-{{ countries.to }} arası
gösteriliyor
</div>
<Pagination :total="countries.total" :items-per-page="countries.per_page"
:default-page="countries.current_page" @update:page="handlePageChange">
<PaginationContent>
<PaginationPrevious class="cursor-pointer">
<ChevronLeft class="h-4 w-4" />
</PaginationPrevious>
<template v-for="(item, index) in countries.links" :key="index">
<PaginationItem v-if="item.url && !isNaN(Number(item.label))" :value="index">
<Button size="icon" variant="ghost" class="h-9 w-9"
:class="{ 'border': item.active }"
@click="handlePageChange(Number(item.label))">
{{ item.label }}
</Button>
</PaginationItem>
</template>
<PaginationNext class="cursor-pointer">
<ChevronRight class="h-4 w-4" />
</PaginationNext>
</PaginationContent>
</Pagination>
</div>
</template>
<div v-else class="mx-2">
<EmptyState title="Ülke Bulunamadı" description="Sistemde henüz hiç ülke oluşturulmamış."
:action-label="'Yeni Ülke Ekle'"
@action="openCreateSheet()" />
</div>
</div>
<Sheet v-model:open="isSheetOpen">
<SheetContent side="right" class="sm:max-w-[400px] p-0 flex flex-col h-full">
<SheetHeader class="p-6 border-b shrink-0">
<SheetTitle class="flex items-center gap-2 text-xl">
<Pencil v-if="editingCountry" class="h-5 w-5 text-primary" />
<Plus v-else class="h-5 w-5 text-primary" />
{{ editingCountry ? 'Ülke Düzenle' : 'Yeni Ülke Ekle' }}
</SheetTitle>
<SheetDescription>
{{ editingCountry ? 'Ülke bilgilerini güncelleyin.' : 'Sisteme yeni bir ülke ekleyin.' }}
</SheetDescription>
</SheetHeader>
<form id="countryForm" class="flex-1 overflow-y-auto p-6 space-y-6" @submit.prevent="submitForm">
<div class="space-y-2">
<Label for="code" class="text-sm font-bold">Kod</Label>
<Input id="code" v-model="form.code" placeholder="TR" class="h-11" />
<InputError :message="form.errors.code" />
</div>
<div class="space-y-2">
<Label for="name" class="text-sm font-bold">Ad</Label>
<Input id="name" v-model="form.name" placeholder="Türkiye" class="h-11" />
<InputError :message="form.errors.name" />
</div>
<div class="space-y-2">
<Label for="sort_order" class="text-sm font-bold">Sıralama</Label>
<Input id="sort_order" v-model.number="form.sort_order" type="number" min="0"
class="h-11" />
<InputError :message="form.errors.sort_order" />
</div>
<div class="flex items-center gap-2">
<Switch id="is_active" v-model:checked="form.is_active" />
<Label for="is_active">Aktif</Label>
</div>
</form>
<SheetFooter class="p-6 border-t bg-muted/10 shrink-0">
<div class="flex w-full flex-col gap-3">
<Button type="submit" form="countryForm" class="w-full h-11" :disabled="form.processing">
<Loader2 v-if="form.processing" class="mr-2 h-4 w-4 animate-spin" />
<Check v-else class="mr-2 h-4 w-4" />
{{ form.processing ? 'Kaydediliyor...' : (editingCountry ? 'Değişiklikleri Kaydet' :
'Ülke Ekle') }}
</Button>
<Button type="button" variant="ghost" class="w-full h-11" @click="isSheetOpen = false">
<X class="mr-2 h-4 w-4" /> İptal
</Button>
<Button v-if="editingCountry && editingCountry.id !== props.defaultCountryId"
type="button" variant="outline"
class="w-full h-11 text-destructive border-destructive/30 hover:bg-destructive/5"
@click="openDeleteDialog">
<Trash2 class="mr-2 h-4 w-4" />
Ülkeyi Sil
</Button>
</div>
</SheetFooter>
</SheetContent>
</Sheet>
<AlertDialog v-model:open="showDeleteDialog">
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Ülkeyi silmek istediğinize emin misiniz?</AlertDialogTitle>
<AlertDialogDescription>
<strong>{{ editingCountry?.name }} ({{ editingCountry?.code }})</strong> kalıcı olarak
silinecektir. Bu
işlem geri alınamaz.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>İptal</AlertDialogCancel>
<AlertDialogAction class="bg-destructive text-destructive-foreground hover:bg-destructive/90"
@click="confirmDelete">
Evet, Sil
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</DefinitionsLayout>
</AppLayout>
</template>
export type Country = {
id: string;
code: string;
name: string;
is_active: boolean;
sort_order: number;
created_at: string;
updated_at: string;
};
export type City = {
id: string;
country_id: string;
name: string;
is_active: boolean;
sort_order: number;
created_at: string;
updated_at: string;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment