228 lines
7.6 KiB
Plaintext
228 lines
7.6 KiB
Plaintext
@{
|
|
ViewData["Title"] = "Booking Managers";
|
|
Layout = "~/Views/Shared/_Layout.cshtml";
|
|
}
|
|
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" />
|
|
|
|
<style>
|
|
.card {
|
|
border-radius: 14px;
|
|
box-shadow: 0 6px 18px rgba(0,0,0,.06);
|
|
border: 0;
|
|
}
|
|
|
|
.card-header {
|
|
background: #f9fbff;
|
|
border-bottom: 1px solid #eef2f7;
|
|
border-top-left-radius: 14px;
|
|
border-top-right-radius: 14px;
|
|
}
|
|
|
|
.it-list {
|
|
max-height: 320px;
|
|
overflow: auto;
|
|
border: 1px solid #eef2f7;
|
|
border-radius: 10px;
|
|
padding: 6px;
|
|
background: #fff;
|
|
}
|
|
|
|
.it-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: .5rem;
|
|
padding: 6px 8px;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.it-item:hover {
|
|
background: #f7f9fc;
|
|
}
|
|
|
|
.it-name {
|
|
font-weight: 600;
|
|
color: #334155;
|
|
}
|
|
|
|
.it-selected {
|
|
min-height: 48px;
|
|
border: 1px dashed #dbe3ef;
|
|
background: #fbfdff;
|
|
border-radius: 10px;
|
|
padding: 8px;
|
|
}
|
|
|
|
.chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: .4rem;
|
|
padding: .28rem .5rem;
|
|
margin: 4px;
|
|
border-radius: 999px;
|
|
background: #eef2ff;
|
|
color: #3949ab;
|
|
font-weight: 600;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.chip-x {
|
|
border: 0;
|
|
background: transparent;
|
|
color: #6b7280;
|
|
line-height: 1;
|
|
font-size: 16px;
|
|
padding: 0 2px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.chip-x:hover {
|
|
color: #111827;
|
|
}
|
|
|
|
.form-text {
|
|
font-size: 12px;
|
|
color: #6c757d;
|
|
}
|
|
</style>
|
|
|
|
<div id="mgrApp" style="max-width:1000px; margin:auto; font-size:13px;">
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="m-0">Booking Managers</h5>
|
|
<div>
|
|
<button class="btn btn-outline-secondary btn-sm" @@click="loadAll" :disabled="busy">Refresh</button>
|
|
<button class="btn btn-primary btn-sm ms-2" @@click="save" :disabled="saving || busy">
|
|
{{ saving ? 'Saving…' : 'Save' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card-body">
|
|
<div v-if="error" class="alert alert-danger py-2">{{ error }}</div>
|
|
<div class="text-muted mb-3">
|
|
<small>Select users who should act as <strong>Managers</strong> for the Room Booking module (approve/reject, manage rooms, etc.).</small>
|
|
</div>
|
|
|
|
<div class="row g-3 align-items-start">
|
|
<!-- LEFT: Search + Available users -->
|
|
<div class="col-md-7">
|
|
<div class="input-group input-group-sm mb-2">
|
|
<span class="input-group-text"><i class="bi bi-search"></i></span>
|
|
<input type="text" class="form-control" placeholder="Search users by name…" v-model.trim="q">
|
|
</div>
|
|
|
|
<div class="it-list">
|
|
<label v-for="u in filteredUsers" :key="'u-'+u.id" class="it-item">
|
|
<input type="checkbox" :value="u.id" v-model="managerIds" />
|
|
<span class="it-name">{{ u.name }}</span>
|
|
<small class="text-muted ms-1">({{ u.email }})</small>
|
|
</label>
|
|
<div v-if="!filteredUsers.length" class="text-muted small p-2">No users match your search.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- RIGHT: Selected chips -->
|
|
<div class="col-md-5">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<strong>Selected ({{ selectedUsers.length }})</strong>
|
|
<button class="btn btn-link btn-sm text-decoration-none"
|
|
@@click="managerIds = []"
|
|
:disabled="!selectedUsers.length">
|
|
Clear all
|
|
</button>
|
|
</div>
|
|
|
|
<div class="it-selected">
|
|
<span v-for="u in selectedUsers" :key="'sel-'+u.id" class="chip">
|
|
{{ u.name }}
|
|
<button class="chip-x" @@click="remove(u.id)" aria-label="Remove">×</button>
|
|
</span>
|
|
<div v-if="!selectedUsers.length" class="text-muted small">Nobody selected yet.</div>
|
|
</div>
|
|
|
|
<div class="mt-2 form-text">
|
|
Managers can: approve/reject bookings, create/update rooms, cancel/un-cancel any booking, etc.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const mgrApp = Vue.createApp({
|
|
data() {
|
|
return {
|
|
busy: false,
|
|
saving: false,
|
|
error: null,
|
|
q: '',
|
|
users: [], // {id, name, email}
|
|
managerIds: [] // [int]
|
|
};
|
|
},
|
|
computed: {
|
|
filteredUsers() {
|
|
const k = (this.q || '').toLowerCase();
|
|
return !k ? this.users : this.users.filter(u =>
|
|
(u.name || '').toLowerCase().includes(k) || (u.email || '').toLowerCase().includes(k)
|
|
);
|
|
},
|
|
selectedUsers() {
|
|
const set = new Set(this.managerIds);
|
|
return this.users.filter(u => set.has(u.id));
|
|
}
|
|
},
|
|
methods: {
|
|
async loadUsers() {
|
|
const r = await fetch('/api/BookingsApi/users');
|
|
if (!r.ok) throw new Error('Failed to load users');
|
|
this.users = await r.json();
|
|
},
|
|
async loadManagers() {
|
|
const r = await fetch('/api/BookingsApi/managers');
|
|
if (!r.ok) throw new Error('Failed to load managers');
|
|
this.managerIds = await r.json(); // array<int>
|
|
},
|
|
async loadAll() {
|
|
try {
|
|
this.busy = true; this.error = null;
|
|
await Promise.all([this.loadUsers(), this.loadManagers()]);
|
|
} catch (e) {
|
|
this.error = e.message || 'Load failed.';
|
|
} finally {
|
|
this.busy = false;
|
|
}
|
|
},
|
|
remove(id) {
|
|
this.managerIds = this.managerIds.filter(x => x !== id);
|
|
},
|
|
async save() {
|
|
try {
|
|
this.saving = true; this.error = null;
|
|
const r = await fetch('/api/BookingsApi/managers', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ userIds: this.managerIds })
|
|
});
|
|
if (!r.ok) {
|
|
const j = await r.json().catch(() => ({}));
|
|
throw new Error(j.message || `Save failed (${r.status})`);
|
|
}
|
|
alert('Managers updated.');
|
|
} catch (e) {
|
|
this.error = e.message || 'Save failed.';
|
|
} finally {
|
|
this.saving = false;
|
|
}
|
|
}
|
|
},
|
|
async mounted() {
|
|
await this.loadAll();
|
|
}
|
|
});
|
|
mgrApp.mount('#mgrApp');
|
|
</script>
|