Update Bookings

This commit is contained in:
Naz 2026-06-04 10:15:56 +08:00
parent 5d62d2e3b9
commit 6822472102
8 changed files with 172 additions and 93 deletions

View File

@ -26,11 +26,17 @@ namespace PSTW_CentralSystem.Areas.Bookings.Controllers
}
// DB-backed manager check (NO Identity roles here)
private Task<bool> IsManagerAsync()
private async Task<bool> IsManagerAsync()
{
var me = GetCurrentUserId();
if (me is null) return Task.FromResult(false);
return _db.BookingManager.AsNoTracking()
if (me is null) return false;
if (User.IsInRole("SuperAdmin") || User.IsInRole("SystemAdmin"))
{
return true;
}
return await _db.BookingManager.AsNoTracking()
.AnyAsync(x => x.UserId == me.Value && x.IsActive);
}

View File

@ -22,6 +22,8 @@ namespace PSTW_CentralSystem.Areas.Bookings.Models
[Required]
public DateTime CreatedUtc { get; set; } = DateTime.UtcNow;
[ForeignKey("CreatedByUserId")]
public int? CreatedByUserId { get; set; }
}
}

View File

@ -243,7 +243,8 @@
box-shadow: 0 2px 6px rgba(0,0,0,.06);
padding: 6px 8px;
font-size: 12px;
overflow: hidden
overflow: hidden;
z-index: 2; /* Tambah baris ini supaya kotak berada DI ATAS garisan */
}
.booking .t {
@ -387,59 +388,66 @@
dowRow.dataset.done = "1";
}
// --- Lookups (rooms + users) ---
let userMap = new Map(), roomMap = new Map(), users = [], rooms = [];
async function loadLookups() {
try {
const r = await fetch(`${api}?scope=${calendarScope}&lookups=1`);
if (!r.ok) throw new Error(`Lookups ${r.status} ${r.statusText}`);
const js = await r.json();
rooms = js.rooms ?? [];
users = js.users ?? [];
} catch (e) {
alertMsg(e.message);
rooms = [];
users = [];
}
// Fill selects
ddlRoom.innerHTML = '<option value="">All rooms</option>';
rooms.forEach(r => {
const id = r.roomId;
const name = r.roomName ?? `Room ${id}`;
if (id == null) return;
const opt = document.createElement("option");
opt.value = id;
opt.textContent = name;
ddlRoom.appendChild(opt);
});
ddlUser.innerHTML = '<option value="">All users</option>';
users.forEach(u => {
const id = u.Id ?? u.id;
const name = u.UserName ?? u.userName ?? "";
const email = u.Email ?? "";
const opt = document.createElement("option");
opt.value = id;
opt.textContent = email ? `${name} (${email})` : name;
ddlUser.appendChild(opt);
});
userMap = new Map(
users.map(u => [
Number(u.Id ?? u.id),
(u.UserName ?? u.userName ?? "") + (u.Email ? ` (${u.Email})` : "")
])
);
roomMap = new Map(
rooms.map(r => {
const id = Number(r.roomId ?? r.RoomId);
const name = r.roomName ?? r.Name ?? r.RoomName ?? `Room ${id}`;
return [id, name];
})
);
// --- Lookups (rooms + users) ---
let userMap = new Map(), roomMap = new Map(), users = [], rooms = [];
async function loadLookups() {
try {
const r = await fetch(`${api}?scope=${calendarScope}&lookups=1`);
if (!r.ok) throw new Error(`Lookups ${r.status} ${r.statusText}`);
const js = await r.json();
rooms = js.rooms ?? [];
users = js.users ?? [];
} catch (e) {
alertMsg(e.message);
rooms = [];
users = [];
}
// Fill selects
ddlRoom.innerHTML = '<option value="">All rooms</option>';
rooms.forEach(r => {
const id = r.roomId;
const name = r.roomName ?? `Room ${id}`;
if (id == null) return;
const opt = document.createElement("option");
opt.value = id;
opt.textContent = name;
ddlRoom.appendChild(opt);
});
ddlUser.innerHTML = '<option value="">All users</option>';
// Filter out the Admin
const nonAdminUsers = users.filter(u => {
const name = u.UserName ?? u.userName ?? "";
return name !== "MAAdmin" && name !== "SysAdmin";
});
nonAdminUsers.forEach(u => {
const id = u.Id ?? u.id;
const name = u.UserName ?? u.userName ?? "";
const email = u.Email ?? "";
const opt = document.createElement("option");
opt.value = id;
opt.textContent = email ? `${name} (${email})` : name;
ddlUser.appendChild(opt);
});
userMap = new Map(
users.map(u => [
Number(u.Id ?? u.id),
(u.UserName ?? u.userName ?? "") + (u.Email ? ` (${u.Email})` : "")
])
);
roomMap = new Map(
rooms.map(r => {
const id = Number(r.roomId ?? r.RoomId);
const name = r.roomName ?? r.Name ?? r.RoomName ?? `Room ${id}`;
return [id, name];
})
);
}
// --- Data fetching for month grid ---
let curMonth = startOfMonth(new Date()), currentData = [];
async function fetchGridData(month) {
@ -556,7 +564,7 @@
function getUserId(b) { const v = [b.requestedByUserId, b.UserId, b.userId, b.targetUserId, b.TargetUserId, b.bookedByUserId, b.BookedByUserId, b.approvedByUserId, b.ApprovedByUserId].find(x => x != null); const n = Number(v); return Number.isNaN(n) ? null : n; }
function openDayBoard(dayDate) {
boardTitle.textContent = `Schedule • ${dayDate.toLocaleDateString()}`;
boardTitle.textContent = `Schedule • ${dayDate.toLocaleDateString('en-GB')}`;
boardFilters.textContent = `Filters: ${ddlRoom.options[ddlRoom.selectedIndex]?.text || "All rooms"}`;
// Left time labels
@ -592,7 +600,7 @@
colsHead.appendChild(h);
});
canvas.style.height = `${TOTAL_INTERVALS * SLOT_PX}px`;
canvas.style.height = `${(TOTAL_INTERVALS + 1) * SLOT_PX}px`;
// Build grid columns
colGrid.innerHTML = "";

View File

@ -352,7 +352,9 @@
// ---------- Lookups ----------
async function loadLookups(selectedRoomId, selectedUserId) {
const res = await fetch(`${api}?lookups=1`);
const res = await fetch(`${api}?lookups=1&onlyActive=true`);
if (!res.ok) throw new Error(await res.text());
const { rooms = [], users = [] } = await res.json();
@ -388,7 +390,13 @@
// Users
const userDdl = document.getElementById("RequestedByUserId");
userDdl.innerHTML = '<option value="">-- Select User --</option>';
users.forEach(u => {
const nonAdminUsers = users.filter(u => {
const name = pick(u, "UserName", "userName") ?? "";
return name !== "MAAdmin" && name !== "SysAdmin";
});
nonAdminUsers.forEach(u => {
const id = pick(u, "Id", "id");
if (id == null) return;

View File

@ -1,12 +1,22 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using System.Security.Claims
@inject PSTW_CentralSystem.DBContext.CentralSystemContext _db
@{
ViewData["Title"] = "Bookings";
Layout = "_Layout";
var isMgr = User?.IsInRole("Manager") == true;
var idStr = User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var meId = int.TryParse(idStr, out var tmp) ? tmp : 0;
bool isAdmin = User?.IsInRole("SuperAdmin") == true || User?.IsInRole("SystemAdmin") == true;
bool isBookingManager = false;
if (meId > 0)
{
isBookingManager = _db.BookingManager.Any(x => x.UserId == meId && x.IsActive);
}
var isMgr = isAdmin || isBookingManager;
}
<style>
@ -148,8 +158,8 @@
}
.col-actions {
width: 150px;
min-width: 150px;
width: 200px;
min-width: 200px;
}
/* Long text cells */
@ -500,29 +510,47 @@
if (!res.ok) return;
const { rooms = [], users = [] } = await res.json();
this.users = users; this.rooms = rooms;
const nonAdminUsers = users.filter(u => {
const name = u.UserName ?? u.userName ?? "";
return name !== "MAAdmin" && name !== "SysAdmin";
});
this.users = nonAdminUsers;
this.rooms = rooms;
const sel = document.getElementById("fltUser");
if (sel) {
const prev = sel.value ?? "";
sel.innerHTML = '<option value="">All users</option>';
users.forEach(u => {
const id = Number(u.Id ?? u.id); if (!id) return;
nonAdminUsers.forEach(u => {
const id = Number(u.Id ?? u.id);
if (!id) return;
const name = u.UserName ?? u.userName ?? "";
const opt = document.createElement("option");
opt.value = String(id); opt.textContent = name;
opt.value = String(id);
opt.textContent = name;
sel.appendChild(opt);
});
if (prev && [...sel.options].some(o => o.value === prev)) { sel.value = prev; this.fltUser = prev; }
if (prev && [...sel.options].some(o => o.value === prev)) {
sel.value = prev;
this.fltUser = prev;
}
}
this.userMap = new Map(users.map(u => [Number(u.Id ?? u.id), (u.UserName ?? u.userName ?? "")]));
this.userMap = new Map(nonAdminUsers.map(u => [Number(u.Id ?? u.id), (u.UserName ?? u.userName ?? "")]));
this.roomMap = new Map(rooms.map(r => {
const id = Number(r.roomId ?? r.RoomId);
const name = r.roomName ?? r.Name ?? r.RoomName ?? `Room ${id}`;
return [id, name];
}));
} catch { }
} catch (e) {
console.error("Error loading lookups:", e);
}
},
async loadList() {

View File

@ -166,7 +166,12 @@
computed: {
filteredUsers() {
const k = (this.q || '').toLowerCase();
return !k ? this.users : this.users.filter(u =>
const nonAdminUsers = this.users.filter(u =>
u.name !== "MAAdmin" && u.name !== "SysAdmin"
);
return !k ? nonAdminUsers : nonAdminUsers.filter(u =>
(u.name || '').toLowerCase().includes(k) || (u.email || '').toLowerCase().includes(k)
);
},

View File

@ -34,6 +34,12 @@ namespace PSTW_CentralSystem.Controllers.API
{
var me = GetCurrentUserId();
if (me is null) return false;
if (User.IsInRole("SuperAdmin") || User.IsInRole("SystemAdmin"))
{
return true;
}
// Dynamic manager list (booking_managers)
return _db.BookingManager.AsNoTracking()
.Any(x => x.UserId == me.Value && x.IsActive);
@ -183,19 +189,31 @@ namespace PSTW_CentralSystem.Controllers.API
// Only an existing manager can edit the list (bootstrap by seeding one row)
if (!IsManager()) return Unauthorized("Managers only.");
var now = DateTime.UtcNow;
var now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, AppTz);
var me = GetCurrentUserId();
// Soft-reset approach: mark all inactive then upsert selected as active
var all = await _db.BookingManager.ToListAsync();
foreach (var bm in all) bm.IsActive = false;
var selected = new HashSet<int>(dto.UserIds.Where(id => id > 0));
foreach (var existing in all)
{
bool shouldBeActive = selected.Contains(existing.UserId);
// If the status is changing
if (existing.IsActive != shouldBeActive)
{
existing.IsActive = shouldBeActive;
existing.CreatedUtc = now; // Update the timestamp
existing.CreatedByUserId = me; // Update who made the change
}
}
foreach (var uid in selected)
{
var existing = all.FirstOrDefault(x => x.UserId == uid);
if (existing is null)
bool exists = all.Any(x => x.UserId == uid);
if (!exists)
{
_db.BookingManager.Add(new BookingManager
{
@ -205,10 +223,6 @@ namespace PSTW_CentralSystem.Controllers.API
CreatedByUserId = me
});
}
else
{
existing.IsActive = true;
}
}
await _db.SaveChangesAsync();
@ -242,7 +256,8 @@ namespace PSTW_CentralSystem.Controllers.API
[FromQuery] int pageSize = 450,
[FromQuery] int? userId = null,
[FromQuery] int? companyId = null,
[FromQuery] int? departmentId = null
[FromQuery] int? departmentId = null,
[FromQuery] bool onlyActive = false
)
{
// ROOMS LIST (admin tooling / open to authenticated)
@ -270,7 +285,7 @@ namespace PSTW_CentralSystem.Controllers.API
{
var roomsAll = await _db.Rooms
.AsNoTracking()
.Where(r => r.IsActive)
//.Where(r => r.IsActive)
.OrderBy(r => r.RoomName)
.Select(r => new { roomId = r.RoomId, roomName = r.RoomName })
.ToListAsync();
@ -337,9 +352,14 @@ namespace PSTW_CentralSystem.Controllers.API
// LOOKUPS PACK
if (lookups == 1)
{
var rooms = await _db.Rooms
.AsNoTracking()
.Where(r => r.IsActive)
var roomsQuery = _db.Rooms.AsNoTracking();
if (onlyActive)
{
roomsQuery = roomsQuery.Where(r => r.IsActive);
}
var rooms = await roomsQuery
.OrderBy(r => r.RoomName)
.Select(r => new { roomId = r.RoomId, roomName = r.RoomName })
.ToListAsync();
@ -347,8 +367,7 @@ namespace PSTW_CentralSystem.Controllers.API
object usersPayload;
if (IsManager())
{
usersPayload = await _db.Users
.AsNoTracking()
usersPayload = await _db.Users.AsNoTracking()
.OrderBy(u => u.FullName)
.Select(u => new { u.Id, UserName = u.FullName, u.Email })
.ToListAsync();

View File

@ -4,6 +4,9 @@ namespace PSTW_CentralSystem.Models
{
public class RoleModel : IdentityRole<int>
{
public int Id { get; set; }
public string? Description { get; set; }
public string? Name { get; set; }
public string? NormalizedName { get; set; }
}
}