Teren-app/resources/js/Components/BasicTable.vue
Simon Pocrnjič 63e0958b66 Dev branch
2025-11-02 12:31:01 +01:00

246 lines
7.7 KiB
Vue

<script setup>
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/Components/ui/table';
import DialogModal from './DialogModal.vue';
import { useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
import TextInput from './TextInput.vue';
import InputLabel from './InputLabel.vue';
import ActionMessage from './ActionMessage.vue';
import PrimaryButton from './PrimaryButton.vue';
import Modal from './Modal.vue';
import SecondaryButton from './SecondaryButton.vue';
import { Button } from '@/Components/ui/button';
const props = defineProps({
title: String,
description: String,
header: Array,
body: Array,
// Make table header sticky while body scrolls
stickyHeader: {
type: Boolean,
default: true
},
editor: {
type: Boolean,
default: false
},
options: {
type: Object,
default: {}
},
// Deprecated: fixed height. Prefer bodyMaxHeight (e.g., 'max-h-96').
bodyHeight: {
type: String,
default: 'h-96'
},
// Preferred: control scrollable body max-height (Tailwind class), e.g., 'max-h-96', 'max-h-[600px]'
bodyMaxHeight: {
type: String,
default: 'max-h-96'
}
});
const drawerUpdateForm = ref(false);
const modalRemove = ref(false);
const formUpdate = (props.editor) ? useForm(props.options.editor_data.form.values) : false;
const formRemove = (props.editor) ? useForm({type: 'soft', key: null}) : false;
const openEditor = (ref, data) => {
formUpdate[ref.key] = ref.val;
for(const [key, value] of Object.entries(data)) {
formUpdate[key] = value;
}
drawerUpdateForm.value = true;
}
const closeEditor = () => {
drawerUpdateForm.value = false;
}
const setUpdateRoute = (name, params = false, index) => {
if(!params){
return route(name, index)
}
return route(name, [params, index]);
}
const update = () => {
const putRoute = setUpdateRoute(
props.options.editor_data.form.route.name,
props.options.editor_data.form.route.params,
formUpdate[props.options.editor_data.form.key]
)
formUpdate.put(putRoute, {
onSuccess: () => {
closeEditor();
formUpdate.reset();
},
preserveScroll: true
});
}
const modalRemoveTitle = ref('');
const closeModal = () => {
modalRemove.value = false;
}
const showModal = (key, title) => {
modalRemoveTitle.value = title;
formRemove.key = key;
modalRemove.value = true;
}
const remove = () => {
const removeRoute = setUpdateRoute(
props.options.editor_data.form.route_remove.name,
props.options.editor_data.form.route_remove.params,
formRemove.key
)
formRemove.delete(removeRoute, {
onSuccess: () => {
closeModal();
formRemove.reset();
},
preserveScroll: true
});
}
</script>
<template>
<div>
<!-- Header -->
<div v-if="title || description" class="mb-4">
<h2 v-if="title" class="text-lg font-semibold text-gray-900">{{ title }}</h2>
<p v-if="description" class="mt-1 text-sm text-gray-600">{{ description }}</p>
</div>
<div :class="['relative rounded-lg border border-gray-200 bg-white shadow-sm overflow-x-auto overflow-y-auto', bodyMaxHeight, stickyHeader ? 'table-sticky' : '']">
<Table class="text-sm">
<TableHeader class="sticky top-0 z-10 bg-gray-50/90 backdrop-blur supports-[backdrop-filter]:bg-gray-50/80 border-b border-gray-200 shadow-sm">
<TableRow class="border-b">
<TableHead
v-for="(h, hIndex) in header"
:key="hIndex"
class="sticky top-0 z-10 uppercase text-xs font-semibold tracking-wide text-gray-700 py-3 first:pl-6 last:pr-6 bg-gray-50/90"
>{{ h.data }}</TableHead>
<TableHead v-if="editor" class="sticky top-0 z-10 w-px text-gray-700 py-3 bg-gray-50/90"></TableHead>
<TableHead v-else class="sticky top-0 z-10 w-px text-gray-700 py-3 bg-gray-50/90" />
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="(row, key, parent_index) in body" :key="key" :class="[row.options.class, 'hover:bg-gray-50/50']">
<TableCell v-for="(col, colIndex) in row.cols" :key="colIndex" class="align-middle">
<a v-if="col.link !== undefined" :class="col.link.css" :href="route(col.link.route, col.link.options)">{{ col.data }}</a>
<span v-else>{{ col.data }}</span>
</TableCell>
<TableCell v-if="editor" class="text-right whitespace-nowrap">
<Button class="mr-2" size="sm" variant="outline" @click="openEditor(row.options.ref, row.options.editable)">Edit</Button>
<Button size="sm" variant="destructive" @click="showModal(row.options.ref.val, row.options.title)">Remove</Button>
</TableCell>
<TableCell v-else />
</TableRow>
</TableBody>
</Table>
<div v-if="!body || body.length === 0" class="p-6 text-center text-sm text-gray-500">No records found.</div>
</div>
</div>
<DialogModal
v-if="editor"
:show="drawerUpdateForm"
@close="drawerUpdateForm = false"
maxWidth="xl"
>
<template #title>Update {{ options.editor_data.title }}</template>
<template #content>
<form @submit.prevent="update" class="pt-2">
<div v-for="(e, eIndex) in options.editor_data.form.el" :key="eIndex" class="col-span-6 sm:col-span-4 mb-4">
<InputLabel :for="e.id" :value="e.label"/>
<TextInput
v-if="e.type === 'text'"
:id="e.id"
:ref="e.ref"
type="text"
:autocomplete="e.autocomplete"
class="mt-1 block w-full text-sm"
v-model="formUpdate[e.bind]"
/>
<select
v-else-if="e.type === 'select'"
class="block w-full border-gray-300 focus:border-indigo-500 focus:ring-indigo-500 rounded-md shadow-sm text-sm"
:id="e.id"
:ref="e.ref"
v-model="formUpdate[e.bind]"
>
<option v-for="(op, opIndex) in e.selectOptions" :key="opIndex" :value="op.val">{{ op.desc }}</option>
</select>
</div>
<div class="flex justify-end mt-6 gap-3">
<ActionMessage :on="formUpdate.recentlySuccessful" class="me-3">
Saved.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': formUpdate.processing }" :disabled="formUpdate.processing">
Save
</PrimaryButton>
</div>
</form>
</template>
</DialogModal>
<Modal
v-if="editor"
:show="modalRemove"
@close="closeModal"
maxWidth="sm"
>
<form @submit.prevent="remove">
<div class="p-6">
<div class="text-base font-medium text-center py-2 mb-4 text-gray-900">
Remove {{ options.editor_data.title }} <b>{{ modalRemoveTitle }}</b>?
</div>
<div class="flex justify-between items-center">
<SecondaryButton type="button" @click="closeModal">
Cancel
</SecondaryButton>
<ActionMessage :on="formRemove.recentlySuccessful" class="me-3">
Deleted.
</ActionMessage>
<PrimaryButton class="bg-red-700" :class="{ 'opacity-25': formRemove.processing }" :disabled="formRemove.processing">
Delete
</PrimaryButton>
</div>
</div>
</form>
</Modal>
</template>
<style scoped>
/* Ensure sticky header remains above scrollable body inside wrapper */
:deep(.table-sticky thead) {
position: sticky;
top: 0;
z-index: 10;
}
:deep(.table-sticky thead th) {
position: sticky;
top: 0;
z-index: 10;
background-color: rgba(249, 250, 251, 0.9); /* gray-50/90 */
backdrop-filter: saturate(180%) blur(5px);
}
/* Maintain column widths alignment when scrollbar appears */
.table-sticky {
/* Make sure the header and body share the same scroll container */
overflow-y: auto;
}
</style>