PSTW_CentralizeSystem/Areas/Bookings/Views/Bookings/Managers.cshtml
2025-11-10 10:26:57 +08:00

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">&times;</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>