diff --git a/core/inventory/serializers.py b/core/inventory/serializers.py
index 9a611f6..2611ce3 100644
--- a/core/inventory/serializers.py
+++ b/core/inventory/serializers.py
@@ -9,20 +9,22 @@ from mail.models import EventAddress
 from tickets.shared_serializers import BasicIssueSerializer
 
 
-class EventAdressSerializer(serializers.ModelSerializer):
-    class Meta:
-        model = EventAddress
-        fields = ['address']
-
-
 class EventSerializer(serializers.ModelSerializer):
-    addresses = EventAdressSerializer(many=True, required=False)
+    addresses = SlugRelatedField(many=True, slug_field='address', queryset=EventAddress.objects.all())
 
     class Meta:
         model = Event
         fields = ['id', 'slug', 'name', 'start', 'end', 'pre_start', 'post_end', 'addresses']
         read_only_fields = ['id']
 
+    def to_internal_value(self, data):
+        data = data.copy()
+        addresses = data.pop('addresses', None)
+        dict = super().to_internal_value(data)
+        if addresses:
+            dict['addresses'] = [EventAddress.objects.get_or_create(address=x)[0] for x in addresses]
+        return dict
+
 
 class ContainerSerializer(serializers.ModelSerializer):
     itemCount = serializers.SerializerMethodField()
diff --git a/core/inventory/tests/v2/test_events.py b/core/inventory/tests/v2/test_events.py
index 48ddb01..bd58515 100644
--- a/core/inventory/tests/v2/test_events.py
+++ b/core/inventory/tests/v2/test_events.py
@@ -47,6 +47,20 @@ class EventTestCase(TestCase):
         self.assertEqual(Event.objects.all()[0].slug, 'EVENT2')
         self.assertEqual(Event.objects.all()[0].name, 'Event 2 new')
 
+    def test_update_event(self):
+        from rest_framework.test import APIClient
+        event = Event.objects.create(slug='EVENT1', name='Event 1')
+        response = APIClient().patch(f'/api/2/events/{event.id}/', {'addresses': ['foo@bar.baz', 'foo1@bar.baz']})
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.json()['slug'], 'EVENT1')
+        self.assertEqual(response.json()['name'], 'Event 1')
+        self.assertEqual(2, len(response.json()['addresses']))
+        self.assertEqual('foo@bar.baz', response.json()['addresses'][0])
+        self.assertEqual('foo1@bar.baz', response.json()['addresses'][1])
+        self.assertEqual(len(Event.objects.all()), 1)
+        self.assertEqual(Event.objects.all()[0].slug, 'EVENT1')
+        self.assertEqual(Event.objects.all()[0].name, 'Event 1')
+
     def test_remove_event(self):
         event = Event.objects.create(slug='EVENT1', name='Event 1')
         Event.objects.create(slug='EVENT2', name='Event 2')
@@ -66,3 +80,15 @@ class EventTestCase(TestCase):
         self.assertEqual('Event', response.json()[0]['name'])
         self.assertEqual(1, len(response.json()[0]['addresses']))
 
+    def test_items_remove_addresss(self):
+        from mail.models import EventAddress
+        from rest_framework.test import APIClient
+        event1 = Event.objects.create(slug='TEST1', name='Event')
+        EventAddress.objects.create(event=event1, address='foo@bar.baz')
+        EventAddress.objects.create(event=event1, address='fo1o@bar.baz')
+        response = APIClient().patch(f'/api/2/events/{event1.id}/', {'addresses': ['foo1@bar.baz']})
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual('TEST1', response.json()['slug'])
+        self.assertEqual('Event', response.json()['name'])
+        self.assertEqual(1, len(response.json()['addresses']))
+        self.assertEqual('foo1@bar.baz', response.json()['addresses'][0])
diff --git a/web/src/components/ExpandableTable.vue b/web/src/components/ExpandableTable.vue
new file mode 100644
index 0000000..6bd5175
--- /dev/null
+++ b/web/src/components/ExpandableTable.vue
@@ -0,0 +1,124 @@
+<template>
+    <table class="table table-striped table-dark">
+        <thead>
+        <tr>
+            <th>
+            </th>
+            <th scope="col" v-for="(column, index) in columns" :key="index">
+                <div class="input-group">
+                    <div class="input-group-prepend">
+                        <button
+                            :class="[ 'btn', column === sortBy ? 'btn-outline-info' : 'btn-outline-secondary' ]"
+                            @click="toggleSort(column)"
+                        >
+                            {{ column }}
+                            <span :class="{ 'text-info': column === sortBy }">
+                                    <font-awesome-icon :icon="getSortIcon(column)"/>
+                                </span>
+                        </button>
+                    </div>
+                    <input
+                        type="text"
+                        class="form-control"
+                        placeholder="filter"
+                        :value="filters[column]"
+                        @input="changeFilter(column, $event.target.value)"
+                    >
+                </div>
+            </th>
+            <th>
+                <slot name="header_actions"/>
+            </th>
+        </tr>
+        </thead>
+        <tbody v-for="(item, index) in internalItems" :key="item[keyName]" style="border-top: none">
+        <tr @click="toggle(index)">
+            <td>
+                <font-awesome-icon icon="angle-right" style="width: 1em;" v-if="collapsed[index]"/>
+                <font-awesome-icon icon="angle-down" style="width: 1em;" v-else/>
+            </td>
+            <td v-for="(column, index) in columns" :key="index">{{ item[column] }}</td>
+            <td>
+                <slot v-bind:id="index" v-bind:item="item" name="actions"/>
+            </td>
+        </tr>
+        <tr v-if="!collapsed[index]">
+            <td :colspan="columns.length + 2">
+                <slot v-bind:id="index" v-bind:item="item" name="detail"/>
+            </td>
+        </tr>
+        </tbody>
+    </table>
+</template>
+
+<script>
+import DataContainer from '@/mixins/data-container';
+import router from '../router';
+import {mapGetters} from "vuex";
+
+export default {
+    name: 'ExpandableTable',
+    mixins: [DataContainer],
+    created() {
+        this.columns.map(e => ({
+            k: e,
+            v: this.$store.getters.getFilters[e]
+        })).filter(e => e.v).forEach(e => this.setFilter(e.k, e.v));
+
+        const query = this.route ? (this.route.query ? this.route.query.collapsed : null) : null;
+        if (query !== null && query !== undefined) {
+            this.collapsed = this.unpackInt(parseInt(query), this.internalItems.length);
+        } else {
+            this.collapsed = this.internalItems.map(() => true);
+        }
+    },
+    data() {
+        return {
+            collapsed: [],
+        };
+    },
+    computed: {
+        ...mapGetters(['route']),
+    },
+    methods: {
+        changeFilter(col, val) {
+            this.setFilter(col, val);
+            let newquery = Object.entries({
+                ...this.$store.getters.getFilters,
+                [col]: val
+            }).reduce((a, [k, v]) => (v ? {...a, [k]: v} : a), {});
+            router.push({query: newquery});
+        },
+        packInt(arr) {
+            return arr.reduce((a, e, i) => a + (e ? 0 : 2 ** i), 0);
+        },
+        unpackInt(n, l) {
+            return [...Array(l)].map((e, i) => (n & 2 ** i) === 0);
+        },
+        toggle(index) {
+            const collapsed = [...this.collapsed]
+            collapsed[index] = !collapsed[index];
+            this.collapsed = collapsed;
+        },
+    },
+    watch: {
+        collapsed: {
+            handler() {
+                const encoded = this.packInt(this.collapsed).toString()
+                if (this.route.query.collapsed !== encoded)
+                    this.$router.push({
+                        ...this.route,
+                        query: {...this.route.query, collapsed: encoded}
+                    });
+            },
+            deep: true,
+        },
+    },
+};
+</script>
+
+<style>
+.table-body-move {
+    transition: transform 1s;
+}
+</style>
\ No newline at end of file
diff --git a/web/src/store.js b/web/src/store.js
index dcc3aea..60dba61 100644
--- a/web/src/store.js
+++ b/web/src/store.js
@@ -339,6 +339,13 @@ const store = createStore({
                 commit('replaceEvents', [...state.events.filter(e => e.eid !== event_id)])
             }
         },
+        async updateEvent({commit, dispatch, state}, {id, partial_event}){
+            console.log(id, partial_event);
+            const {data, success} = await http.patch(`/2/events/${id}/`, partial_event, state.user.token);
+            if (success) {
+                commit('replaceEvents', [...state.events.filter(e => e.eid !== id), data])
+            }
+        },
         async fetchTicketStates({commit, state, getters}) {
             if (!state.user.token) return;
             if (state.fetchedData.states > Date.now() - 1000 * 60 * 60 * 24) return;
diff --git a/web/src/views/admin/Events.vue b/web/src/views/admin/Events.vue
index 1aab608..e2ff952 100644
--- a/web/src/views/admin/Events.vue
+++ b/web/src/views/admin/Events.vue
@@ -1,50 +1,102 @@
 <template>
-    <Table
-        :columns="['slug', 'name']"
-        :items="events"
-        :keyName="'slug'"
-    >
-        <template v-slot:header_actions>
-            <button class="btn btn-success" @click.prevent="openAddEventModal">
-                <font-awesome-icon icon="plus"/>
-                Create Event
-            </button>
-        </template>
-        <template v-slot:actions="{ item }">
-            <div class="btn-group">
-                <button class="btn btn-secondary" @click.stop="changeEvent(item)">
-                    <font-awesome-icon icon="archive"/>
-                    use
+    <AsyncLoader :loaded="events.length > 0">
+        <ExpandableTable v-if="!!events" :columns="['slug', 'name']" :items="events" :keyName="'slug'">
+            <template v-slot:header_actions>
+                <button class="btn btn-success" @click.prevent="openAddEventModal">
+                    <font-awesome-icon icon="plus"/>
+                    Create Event
                 </button>
-                <button class="btn btn-danger" @click.stop="safeDeleteEvent(item.eid)">
-                    <font-awesome-icon icon="trash"/>
-                    delete
-                </button>
-            </div>
-        </template>
-    </Table>
+            </template>
+            <template v-slot:actions="{ item }">
+                <div class="btn-group">
+                    <button class="btn btn-secondary" @click.stop="changeEvent(item)" style="white-space: nowrap;">
+                        <font-awesome-icon icon="archive"/>&nbsp;use
+                    </button>
+                    <button class="btn btn-danger" @click.stop="safeDeleteEvent(item.eid)" style="white-space: nowrap;">
+                        <font-awesome-icon icon="trash"/>&nbsp;delete
+                    </button>
+                </div>
+            </template>
+            <template v-slot:detail="{id, item }">
+                <div class="row">
+                    <div class="col">
+                        <input type="date" class="form-control form-control-sm" title="Buildup Start"
+                               v-model="item.pre_start" disabled style="opacity: 1"
+                               :max="item.start" @focus="prepare_date_field">
+                    </div>
+                    <div class="col">
+                        <input type="date" class="form-control form-control-sm" title="Official Event Start"
+                               v-model="item.start" disabled style="opacity: 1"
+                               :min="item.pre_start" :max="item.end" @focus="prepare_date_field">
+                    </div>
+                    <div class="col">
+                        <input type="date" class="form-control form-control-sm" title="Official Event End"
+                               v-model="item.end" disabled style="opacity: 1"
+                               :min="item.start" :max="item.post_end" @focus="prepare_date_field">
+                    </div>
+                    <div class="col">
+                        <input type="date" class="form-control form-control-sm" title="Teardown End"
+                               v-model="item.post_end" disabled style="opacity: 1"
+                               :min="item.end" @focus="prepare_date_field">
+                    </div>
+                </div>
+                <div class="mt-3">
+                    <label class="mr-3">Addresses: </label>
+                    <div v-for="(address, a_id) in item.addresses" class="btn-group btn-group-sm mr-3"
+                         @click.stop="deleteAddress(id, a_id)">
+                        <button class="btn btn-secondary" disabled style="opacity: 1">
+                            {{ address }}
+                        </button>
+                        <button class="btn btn-danger">
+                            <font-awesome-icon icon="trash"/>
+                        </button>
+                    </div>
+                    <div class="btn-group btn-group-sm">
+                        <input type="text" v-model="new_address[id]">
+                        <button class="btn btn-secondary" @click.stop="addAddress(id)" style="white-space: nowrap;">
+                            <font-awesome-icon icon="envelope"/>&nbsp;add
+                        </button>
+                    </div>
+                </div>
+            </template>
+        </ExpandableTable>
+    </AsyncLoader>
 </template>
 
 <script>
 import {mapActions, mapMutations, mapState} from 'vuex';
-import Table from '@/components/Table';
+import ExpandableTable from "@/components/ExpandableTable.vue";
+import AsyncLoader from "@/components/AsyncLoader.vue";
 
 export default {
     name: 'Events',
-    components: {Table},
+    components: {AsyncLoader, ExpandableTable},
     computed: mapState(['events']),
+    data() {
+        return {new_address: []}
+    },
     methods: {
-        ...mapActions(['changeEvent', 'deleteEvent']),
+        ...mapActions(['changeEvent', 'deleteEvent', 'updateEvent']),
         ...mapMutations(['openAddEventModal']),
         safeDeleteEvent(id) {
             if (confirm('do you want to completely delete this event and related data?')) {
                 this.deleteEvent(id)
             }
         },
+        addAddress(id) {
+            const a = this.new_address[id];
+            if (!this.events[id].addresses.includes(a))
+                this.events[id].addresses.push(a)
+            this.new_address[id] = ""
+            this.updateEvent({id: this.events[id].eid, partial_event: {addresses: this.events[id].addresses}});
+        },
+        deleteAddress(id, a_id) {
+            this.events[id].addresses = this.events[id].addresses.filter((e, i) => i !== a_id);
+            this.updateEvent({id: this.events[id].eid, partial_event: {addresses: this.events[id].addresses}});
+        }
     },
 };
 </script>
 
 <style scoped>
-
 </style>
\ No newline at end of file