VANITY MUNICIPAL POLICE

Welcome to the Vanity Municipal Police official website. In this website, you will be able to find informative resources to help you start your career in our police service.

Kind regards,VMP Senior Leadership.
VMP PERFORMANCE OVERVIEW

Become part of a professional British police service. We are always looking for great talent with the right qualities.

0Service Hours
0Command Members
0Active Members
0Specialist Divisions
(function () { function animateCounters() { const counters = document.querySelectorAll(".counter"); counters.forEach(counter => { if (counter.dataset.animated === "true") return; const target = parseInt(counter.getAttribute("data-target"), 10) || 0; const duration = 1600; const startTime = performance.now(); counter.dataset.animated = "true"; function updateCounter(currentTime) { const progress = Math.min((currentTime - startTime) / duration, 1); const eased = 1 - Math.pow(1 - progress, 3); const value = Math.floor(eased * target); counter.textContent = value.toLocaleString(); if (progress < 1) { requestAnimationFrame(updateCounter); } else { counter.textContent = target.toLocaleString() + "+"; } } requestAnimationFrame(updateCounter); }); } const observer = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { animateCounters(); } }); }, { threshold: 0.4 }); document.querySelectorAll(".vmp-stats-section").forEach(section => { observer.observe(section); }); })();
VMP POLICY ARCHIVE

USE OF FORCE

COMMON LAW

An officer may use force to protect themselves or another.

SECTION 3, CRIMINAL LAW ACT 1967

Officers may use reasonable force in the circumstances to prevent crime.

SECTION 117 POLICE AND CRIMINAL EVIDENCE ACT 1984

When an arrest is made reasonable force may be used.

HUMAN RIGHTS ACT 1998

Force must always be proportionate, legal, officers are accountable and it must have been necessary.

PLANE [PROPORTIONALITY]

Action taken must be proportionate to the threat in all circumstances. The amount of force used must be the minimum required to achieve a lawful objective.

PLANE [LEGALITY]

There must be legal basis for taking the action. This can derive from common law.

PLANE [ACCOUNTABILITY]

Officers should record their decision and must be able to account for why they chose a particular course of action and, in some cases, what other options may have been available and why these were not chosen.

PLANE [NECESSITY]

The action taken by the officer must have been necessary to carry out their duty.

PLANE [ETHICAL]

The actions should be in accordance with the principles of conduct [ethical policing] that are considered correct, and appropriate for the conduct becoming of an officer.

REASONABLE FORCE

Officers are required to use necessary force to achieve a lawful objective in conjunction with common law. When force is not reasonable and proportionate, the officer is liable to criminal or misconduct proceedings.

NATIONAL DECISION MODEL

  • Code of Ethics
  • Information
  • Assessment
  • Powers and Policy
  • Options
  • Action and Review
← Back
VMP POLICY ARCHIVE

SEARCH GUIDELINE

The primary purpose of stop and search powers is to enable our officers to allay or confirm suspicions about individuals without exercising their power of arrest.

Where an officer suspects a person is, has been or is about to be involved in unlawful activity, or where they are seeking information about a person’s whereabouts and intentions, they may first stop the person and ask some questions so that the person has an opportunity to account for themselves. The person is free to leave at this stage and not obliged to answer the questions.

If the officer has reasonable grounds to suspect that a person or vehicle is carrying an unlawful item, they may decide to carry out a stop and search. This means that the person can be detained for the purpose of the search. This is not an arrest, but the person is not free to leave until the search is either completed or not proceeded with, and the officer is empowered to use reasonable force if necessary to effect the search.

GOWISLEY [GROUNDS]

A clear explanation of the reasons for the search.

GOWISLEY [OBJECTIVE]

What you will be searching for.

GOWISLEY [WARRANT]

Warrant card to be shown to the detainee.

GOWISLEY [IDENTIFICATION]

Name and badge number must be stated.

GOWISLEY [STATION]

Where the officer is stationed at.

GOWISLEY [LEGISLATION]

What power has been exercised to conduct the stop search.

GOWISLEY [ENTITLEMENT]

The detainee is entitled to a copy of the search.

GOWISLEY [YOU]

Offender must be informed they are detained.

← Back
VMP POLICY ARCHIVE

EVIDENCE

EVIDENCE STORAGE

All items seized from an offender is evidence. This must be placed into an evidence locker as soon as possible and must be done before leaving the station.

BROKEN & DECAYED WEAPONS

All broken weapons are considered evidence and remain as police property. This must be placed into an evidence locker with the 'Broken Police Equipment' tag.

PHOTOGRAPHICAL & VIDEOGRAPHICAL EVIDENCE

[BWV] Body worn video evidence must be available for all charges placed against the [PIC] person in custody. This also includes photographic evidence. Evidence requirement is subject to the incident report and the offence(s) committed by the offender(s). BWV [body worn video] evidence is only submissible if and only you have your body worn turned on. Vehicle dashcam is available in all police vehicles automatically, this does not require any activation of any cameras and may be used in any evidence.

← Back
VMP POLICY ARCHIVE

OFF-DUTY GUIDELINE

NOTIFICATION

Notify your line manager that you are going off-duty before attempting to leave the station off the clock.

EQUIPMENT STORAGE

You cannot bring any police equipment with you whilst you are off-duty. Ensure to 'store as LEO' your equipment.

UNIFORM

You cannot go off-duty and remain in your police uniform. Before leaving the station, change into civilian clothing and clock yourself off-duty.

BECOMING OF AN OFFICER

Off-duty, you still are a member of the police service. Wearing offensive or incriminating clothing is against conduct.

← Back
VMP POLICY ARCHIVE

ARREST POWERS & GUIDELINE

SURRENDER POSITION

An offender must be in a surrender position to place cuffs on. It is only under this requirement when you are authorised to cuff an offender. Examples of surrender positions are below.

RAGDOLLED

The offender is on the floor after an officer has tackled them, tasered them, or if they are knocked out.

VOLUNTARY SURRENDER

The offender voluntary surrendered and they placed their hands up or they are on their knees.

CONSENT

The offender has asked you or told you to cuff them therefore voluntarily surrendering.

SECTION 24 POLICE AND CRIMINAL EVIDENCE ACT 1984

  • Inform the offender they are under arrest
  • List the charges for the arrest
  • Caution the offender using then "when" caution
  • Inform the offender the necessity for the arrest

ARREST RESPONSIBILITY

The arresting officer [AO] is responsible for the arrest and holds the duty of care up until the offender has been processed or handed over to another officer.

ARREST HANDOVER

Until the handover has been confirmed by the other officer(s), the arresting officer is and will remain responsible for the processing of the offender they originally arrested.

SENTENCING GUIDELINES

Police constables require authorisation to book an offender who is in process of being charged for a minimum of 80 months. This authorisation can be granted by a supervisor.

CUSTODY HOLD

Police constables are authorised to retain an offender in the custody for a maximum of 25 minutes, to extend the custody retainment it is required to receive authorisation from your supervisor and a valid reason must be provided.

← Back
VMP POLICY ARCHIVE

DISCIPLINE & STANDARDS

CRIME

Officers are prohibited from committing an offence, small or great. Officers are liable to disciplinary hearings and arrests if an offence is committed.

POLICE EQUIPMENT

Police equipment withdrawn from the police armoury follows an automatic tracking system [ATS] on the PNC. Misuse of police equipment will result in an internal investigation.

← Back
VMP POLICY ARCHIVE

ETHICAL POLICING

ETHICAL POLICING

Ethical policing derives from courage, respect, empathy, and public service. These principles help make and reflect an officers professional decisions.

COURAGE

Making, communicating and being accountable for decisions, and standing against anything that could bring our profession into disrepute.

RESPECT & EMPATHY

Encouraging, listening to and understanding the views of others, and seeking to recognise and respond to the physical, mental and emotional challenges that we and other people may face.

PUBLIC SERVICE

Working in the public interest, fostering public trust and confidence, and taking pride in providing an excellent service to the public.

CLOSING STATEMENT

Ethical policing principles help people in policing do the right things, in the right way, for the right reasons. As a policing professional, you are expected to carry out our responsibilities in an honest and professional manner. This means demonstrating care, attention and diligence, as well as fulfilling your role to the best of your ability at all times.

← Back
VMP HIERARCHY STRUCTURE

RANKS & DIVISIONS

View the official Vanity Municipal Police leadership structure and rank holders.

CHIEF OFFICERS
COM

COMMISSIONER

Senior Management Board

J. LUTHER

DCOM

DEPUTY COMMISSIONER

Senior Management Board

VACANT

ACOM

ASSISTANT COMMISSIONER

Senior Management Board

VACANT

SENIOR LEADERSHIP
CSUPT

CHIEF SUPERINTENDENT

SENIOR LEADERSHIP

VACANT

SUPT

SUPERINTENDENT

SENIOR LEADERSHIP

VACANT

CI

CHIEF INSPECTOR

OPERATIONAL COMMAND

C. KRAY

TEAM LEADS & SUPERVISORS
INSP

INSPECTOR

TEAM LEADER

J. JAMES

R. MURPHY

J. FINCH

SGT

SERGEANT

SUPERVISOR

B. LOXTON

J. DUNN

H. ASQUITH

M. DOWN

OFFICERS
PC

CONSTABLE

FRONTLINE OFFICER
PPC

PROBATIONARY CONSTABLE

PROBATIONARY OFFICER
← Back
VMP RECRUITMENT

POLICE APPLICATION

Complete the Vanity Municipal Police application form. All submissions are sent directly for review.

An experienced officer is first on scene, you are dispatched to assist this experienced officer. Upon arrival, you are told by the experienced officer to arrest this person. What would you do?

(function() { function initAppForm() { if (!window.supabase) { setTimeout(initAppForm, 300); return; } const client = window.supabase.createClient( 'https://aexslkhqwlgjfvqkdtqb.supabase.co', 'sb_publishable_OyUN0atXOWXCfw6svCVpHA_7flXAiqA' ); const form = document.getElementById('vmpApplicationForm'); const status = document.getElementById('vmpAppStatus'); form.addEventListener('submit', async function(e) { e.preventDefault(); status.textContent = 'Submitting application...'; const formData = new FormData(form); const payload = { character_name: formData.get('character_name'), discord_name: formData.get('discord_name'), age: formData.get('age'), captcha: formData.get('captcha'), consent: formData.get('consent'), experience: formData.get('experience'), availability: formData.get('availability'), why_join: formData.get('why_join'), scenario_answer: formData.get('scenario_answer'), extra_notes: "Arrest scenario answer: " + (formData.get('arrest_scenario') || "N/A") + "\n\nExtra notes: " + (formData.get('final_extra_notes') || "N/A") }; const { error } = await client .from('vmp_applications') .insert([payload]); if (error) { status.textContent = 'Submission failed: ' + error.message; return; } form.reset(); status.textContent = 'Application submitted successfully. Command will review your application.'; }); } initAppForm(); })();
VMP POLICE DATABASE

MOST WANTED OFFENDER

LOADING
NO IMAGE
THREAT: LOADING

NAME

Loading...

ALIAS

Loading...

LAST SEEN

Loading...

VEHICLE

Loading...

CHARGES

    ADVISORY

    Loading...

    SYNCING DATABASE...

    (function () { const SUPABASE_URL = "https://aexslkhqwlgjfvqkdtqb.supabase.co"; const SUPABASE_ANON_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFleHNsa2hxd2xnamZ2cWtkdHFiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg4Nzk5MDMsImV4cCI6MjA5NDQ1NTkwM30.neZVBV6tyjjZtkRYX4lmRn_MyFgdRTUQv3rNS8HwpBE"; function setText(id, text) { const el = document.getElementById(id); if (el) el.textContent = text; } function setThreatStyle(level) { const threat = document.getElementById("mw-threat"); if (!threat) return; threat.classList.remove( "threat-low", "threat-medium", "threat-high", "threat-extreme" ); const clean = (level || "").toUpperCase(); if (clean === "LOW") { threat.classList.add("threat-low"); } else if (clean === "MEDIUM") { threat.classList.add("threat-medium"); } else if (clean === "EXTREME") { threat.classList.add("threat-extreme"); } else { threat.classList.add("threat-high"); } } function startMostWanted() { let tries = 0; const waitForSupabase = setInterval(function () { tries++; if (window.supabase) { clearInterval(waitForSupabase); loadMostWanted(); } if (tries >= 30) { clearInterval(waitForSupabase); setText("mw-name", "DATABASE ERROR"); setText("mw-alias", "Supabase failed to load"); setText("mw-status", "OFFLINE"); setText("mw-threat", "THREAT: UNKNOWN"); setText("mw-advisory", "Check your Supabase key."); setText("mw-updated", "CONNECTION FAILED"); } }, 200); } async function loadMostWanted() { try { const client = window.supabase.createClient( SUPABASE_URL, SUPABASE_ANON_KEY ); const { data, error } = await client .from("most_wanted") .select("*") .order("updated_at", { ascending: false }) .limit(1) .maybeSingle(); if (error) throw error; if (!data) { setText("mw-name", "NO ACTIVE BOLO"); setText("mw-alias", "No offender listed"); setText("mw-last-seen", "N/A"); setText("mw-vehicle", "N/A"); setText("mw-advisory", "Database connected successfully."); setText("mw-status", "ONLINE"); setText("mw-threat", "THREAT: N/A"); setText("mw-updated", "DATABASE ONLINE"); return; } setText("mw-name", data.name || "Unknown"); setText("mw-alias", data.alias || "No known alias"); setText("mw-last-seen", data.last_seen || "Unknown"); setText("mw-vehicle", data.vehicle || "Unknown"); setText("mw-advisory", data.advisory || "No advisory issued."); setText("mw-status", data.status || "ACTIVE"); const threatLevel = data.threat_level || "HIGH"; setText("mw-threat", "THREAT: " + threatLevel); setThreatStyle(threatLevel); const chargesList = document.getElementById("mw-charges"); if (chargesList) { chargesList.innerHTML = ""; const charges = (data.charges || "") .split(",") .map(c => c.trim()) .filter(Boolean); charges.forEach(charge => { const li = document.createElement("li"); li.textContent = charge; chargesList.appendChild(li); }); } const imageBox = document.getElementById("mw-image"); if (imageBox && data.image_url && data.image_url.trim() !== "") { imageBox.innerHTML = ""; imageBox.style.backgroundImage = "url('" + data.image_url + "')"; imageBox.classList.add("has-img"); } if (data.updated_at) { const date = new Date(data.updated_at); setText( "mw-updated", "LAST UPDATED: " + date.toLocaleString("en-GB") ); } } catch (err) { console.error(err); setText("mw-name", "DATABASE ERROR"); setText("mw-alias", "Connection issue"); setText("mw-advisory", err.message || "Unable to load offender."); setText("mw-status", "OFFLINE"); setText("mw-threat", "THREAT: UNKNOWN"); setText("mw-updated", "CONNECTION FAILED"); } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", startMostWanted); } else { startMostWanted(); } })();
    VMP HONOUR WALL

    HALL OF HONOUR

    Recognising officers who have shown dedication, professionalism, leadership, and outstanding service within Vanity Municipal Police.

    Leadership AwardStatus: Pending

    John Doe

    Coming Soon

    Award record awaiting command update.

    Activity AwardStatus: Pending

    John Doe

    Coming Soon

    Award record awaiting command update.

    Bravery AwardStatus: Pending

    John Doe

    Coming Soon

    Award record awaiting command update.

    VMP COMMAND RESPONSE

    LOADING ANNOUNCEMENT...

    (function () { const VMP_SUPABASE_URL = "https://aexslkhqwlgjfvqkdtqb.supabase.co"; const VMP_SUPABASE_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFleHNsa2hxd2xnamZ2cWtkdHFiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg4Nzk5MDMsImV4cCI6MjA5NDQ1NTkwM30.neZVBV6tyjjZtkRYX4lmRn_MyFgdRTUQv3rNS8HwpBE"; function el(id) { return document.getElementById(id); } function startAnnouncementSystem() { if (!window.supabase) { setTimeout(startAnnouncementSystem, 150); return; } const vmpAnnSB = window.supabase.createClient(VMP_SUPABASE_URL, VMP_SUPABASE_KEY); async function loadAnnouncement() { const { data, error } = await vmpAnnSB .from("vmp_announcements") .select("*") .order("created_at", { ascending: false }) .limit(1) .maybeSingle(); if (error) { el("vmpAnnTitle").textContent = "Unable to load bulletin"; el("vmpAnnMsg").textContent = "Please refresh or contact command."; return; } if (!data) { el("vmpAnnTitle").textContent = "No Current Bulletin"; el("vmpAnnMsg").textContent = "Suggestions are currently open for review."; return; } el("vmpAnnTitle").textContent = data.title || "Command Bulletin"; el("vmpAnnMsg").textContent = data.message || ""; const purge = el("vmpPurgeStatus"); const review = el("vmpReviewDate"); if (data.board_purged) { purge.style.display = "inline-flex"; purge.textContent = "Board Purged"; } else { purge.style.display = "none"; } if (data.review_date) { review.style.display = "inline-flex"; review.textContent = "NEXT REVIEW: " + data.review_date; } else { review.style.display = "none"; } const acceptedWrap = el("vmpAcceptedWrap"); const acceptedList = el("vmpAcceptedList"); acceptedList.innerHTML = ""; if (data.accepted_suggestions) { acceptedWrap.style.display = "block"; data.accepted_suggestions.split("\n").filter(Boolean).forEach(item => { const li = document.createElement("li"); li.textContent = item.trim(); acceptedList.appendChild(li); }); } else { acceptedWrap.style.display = "none"; } } async function checkCommandAccess() { const { data: sessionData } = await vmpAnnSB.auth.getSession(); const user = sessionData?.session?.user; if (!user || !user.email) return; const { data } = await vmpAnnSB .from("vmp_terminal_permissions") .select("id") .eq("email", user.email.toLowerCase()) .eq("module", "suggestions") .maybeSingle(); if (data) { el("vmpAnnEditor").style.display = "block"; } } el("publishAnn").addEventListener("click", async function () { const status = el("annStatus"); status.textContent = "Publishing..."; const { data: sessionData } = await vmpAnnSB.auth.getSession(); const user = sessionData && sessionData.session && sessionData.session.user; const shouldPurge = el("annPurged").checked; const payload = { title: el("annTitle").value.trim(), message: el("annMessage").value.trim(), accepted_suggestions: el("annAccepted").value.trim(), review_date: el("annReview").value.trim(), board_purged: shouldPurge, created_by: user && user.email ? user.email : "Unknown" }; if (!payload.title || !payload.message) { status.textContent = "Please complete title and message."; return; } const { error: annError } = await vmpAnnSB .from("vmp_announcements") .insert(payload); if (annError) { status.textContent = "Failed to publish. Make sure you're logged in as command."; return; } if (shouldPurge) { status.textContent = "Announcement published. Purging suggestions..."; const { error: purgeError } = await vmpAnnSB .from("vmp_suggestions") .delete() .not("id", "is", null); if (purgeError) { status.textContent = "Announcement published, but purge failed. Check delete policy."; await loadAnnouncement(); return; } } status.textContent = shouldPurge ? "Announcement published and suggestions purged." : "Announcement published."; el("annTitle").value = ""; el("annMessage").value = ""; el("annAccepted").value = ""; el("annReview").value = ""; el("annPurged").checked = false; await loadAnnouncement(); if (window.loadSuggestions) { window.loadSuggestions(); } }); loadAnnouncement(); checkCommandAccess(); } startAnnouncementSystem(); })();
    VMP POLICE

    SUGGESTIONS FORUM

    Share ideas, vote on feedback, and follow official command responses.

    POST A NEW SUGGESTION

    COMMUNITY SUGGESTIONS

    Loading suggestions...

    Loading suggestions...
    (function () { function buildRosterUrl() { return ['https://', 'rostervmp', '.', 'netlify', '.', 'app', '/'].join(''); } document.addEventListener('click', function (event) { const rosterCard = event.target.closest('[data-roster-link]'); if (!rosterCard) return; event.preventDefault(); window.open(buildRosterUrl(), '_blank', 'noopener'); }); })();
    document.querySelectorAll("#vmp-officer-quicklinks button[data-link]").forEach(function(btn) { btn.addEventListener("click", function() { var link = btn.getAttribute("data-link"); navigator.clipboard.writeText(link).then(function() { var status = document.getElementById("vmpOqlStatus"); status.textContent = "Copied: " + link; setTimeout(function() { status.textContent = ""; }, 2500); }); }); });
    VMP RECRUITMENT

    DIVISION RECRUITMENT STATUS

    Current recruitment availability across Vanity Municipal Police operational divisions.

    Response

    OPEN

    Instructions on how to join the division will be provided when the server is open.

    Roads Policing

    OPEN

    Instructions on how to join the division will be provided when the server is open.

    Armed Response

    OPEN

    Instructions on how to join the division will be provided when the server is open.

    Criminal Investigations

    OPEN

    Instructions on how to join the division will be provided when the server is open.

    Serious Organised Crime Unit (SOCU)

    CLOSED

    Instructions on how to join the division will be provided when the server is open.

    Dog Support Unit

    OPEN

    Instructions on how to join the division will be provided when the server is open.

    Specialist Firearms

    OPEN

    Instructions on how to join the division will be provided when the server is open.

    VMP POST-INDUCTION EXAM

    Complete the assessment below. Your answers will be submitted directly for review.

    1. In PLANE, what does [A] stand for?

    2. The power to arrest an individual without a warrant is?

    3. When conducting a stop and search you must follow [GOWISLEY], please explain them below.

    4. We at VMP value ethical policing very highly, what are the 4 things that derive from ethical policing?

    (function () { function initExam() { if (!window.supabase) { setTimeout(initExam, 300); return; } const client = window.supabase.createClient( 'https://aexslkhqwlgjfvqkdtqb.supabase.co', 'sb_publishable_OyUN0atXOWXCfw6svCVpHA_7flXAiqA' ); const form = document.getElementById('vmpExamForm'); const status = document.getElementById('vmpExamStatus'); form.addEventListener('submit', async function(e) { e.preventDefault(); const candidateName = document.getElementById('candidateName').value.trim(); const discordName = document.getElementById('discordName').value.trim(); const callsign = document.getElementById('callsign').value.trim(); const division = document.getElementById('division').value.trim(); const questions = document.querySelectorAll('.vmp-question'); let score = 0; let totalAutoMarked = 0; const answers = []; questions.forEach((question, index) => { const title = question.querySelector('h3').textContent.trim(); const correct = question.getAttribute('data-correct'); if (question.classList.contains('written')) { const textarea = question.querySelector('textarea'); answers.push({ question: title, type: 'written', answer: textarea.value.trim() }); } else { const selected = question.querySelector('input[type="radio"]:checked'); const answer = selected ? selected.value : ''; totalAutoMarked++; if (answer === correct) score++; answers.push({ question: title, type: 'multiple_choice', answer: answer, correct_answer: correct, is_correct: answer === correct }); } }); if (!candidateName || !discordName) { status.textContent = 'Please enter your character name and Discord name.'; return; } status.textContent = 'Submitting assessment...'; const passStatus = 'Pending Review'; const { error } = await client .from('vmp_exam_submissions') .insert([{ candidate_name: candidateName, discord_name: discordName, callsign: callsign, division: division, score: score, total_questions: totalAutoMarked, pass_status: passStatus, answers: answers }]); if (error) { status.textContent = 'Submission failed: ' + error.message; return; } status.textContent = 'Assessment submitted successfully. Command will review your answers.'; form.reset(); }); } initExam(); })();
    ACCESS RESTRICTED

    COMMAND LOGIN REQUIRED

    You must be logged into the Command Portal to review induction exam submissions.

    (function () { function initExamReview() { if (!window.supabase) { setTimeout(initExamReview, 300); return; } const client = window.supabase.createClient( 'https://aexslkhqwlgjfvqkdtqb.supabase.co', 'sb_publishable_OyUN0atXOWXCfw6svCVpHA_7flXAiqA' ); const locked = document.getElementById('vmpExamLocked'); const content = document.getElementById('vmpExamReviewContent'); const panel = document.getElementById('vmpReviewPanel'); async function isCommandAuthenticated() { function findEmail(value) { if (!value) return ""; const match = String(value).match(/[A-Z0-9._%+-]+@vmpcommand\.co\.uk/i); return match ? match[0].toLowerCase().trim() : ""; } let email = ""; for (let i = 0; i < localStorage.length; i++) { const found = findEmail(localStorage.getItem(localStorage.key(i))); if (found) { email = found; break; } } if (!email) return false; const { data } = await client .from('vmp_terminal_permissions') .select('id') .eq('email', email) .eq('module', 'exams') .maybeSingle(); return !!data; } function statusClass(status) { const s = (status || '').toLowerCase(); if (s.includes('pass')) return 'vmp-passed'; if (s.includes('fail')) return 'vmp-failed'; return 'vmp-pending'; } async function updateStatus(id, status) { await client .from('vmp_exam_submissions') .update({ pass_status: status }) .eq('id', id); loadSubmissions(); } function createCard(item) { const card = document.createElement('div'); card.className = 'vmp-review-card'; const top = document.createElement('div'); top.className = 'vmp-review-top'; const left = document.createElement('div'); const title = document.createElement('div'); title.className = 'vmp-review-title'; title.textContent = item.candidate_name || 'Unknown'; const meta = document.createElement('div'); meta.className = 'vmp-review-meta'; meta.innerHTML = ` Discord: ${item.discord_name || 'N/A'}
    Callsign: ${item.callsign || 'N/A'}
    Division: ${item.division || 'N/A'}
    Score: ${item.score || 0}/${item.total_questions || 0} `; left.appendChild(title); left.appendChild(meta); const status = document.createElement('div'); status.className = 'vmp-review-status ' + statusClass(item.pass_status); status.textContent = item.pass_status || 'Pending Review'; top.appendChild(left); top.appendChild(status); const actions = document.createElement('div'); actions.className = 'vmp-review-actions'; const viewBtn = document.createElement('button'); viewBtn.className = 'vmp-toggle-btn'; viewBtn.textContent = 'View Answers'; const passBtn = document.createElement('button'); passBtn.className = 'vmp-pass-btn'; passBtn.textContent = 'Pass'; const failBtn = document.createElement('button'); failBtn.className = 'vmp-fail-btn'; failBtn.textContent = 'Fail'; actions.appendChild(viewBtn); actions.appendChild(passBtn); actions.appendChild(failBtn); const answers = document.createElement('div'); answers.className = 'vmp-answer-list'; (item.answers || []).forEach(answer => { const answerBox = document.createElement('div'); answerBox.className = 'vmp-answer-item'; const q = document.createElement('div'); q.className = 'vmp-answer-question'; q.textContent = answer.question || 'Question'; const a = document.createElement('div'); a.className = 'vmp-answer-text'; a.textContent = answer.answer || 'No answer submitted'; answerBox.appendChild(q); answerBox.appendChild(a); answers.appendChild(answerBox); }); viewBtn.onclick = function() { const open = answers.style.display === 'block'; answers.style.display = open ? 'none' : 'block'; viewBtn.textContent = open ? 'View Answers' : 'Hide Answers'; }; passBtn.onclick = function() { updateStatus(item.id, 'Passed'); }; failBtn.onclick = function() { updateStatus(item.id, 'Failed'); }; card.appendChild(top); card.appendChild(actions); card.appendChild(answers); return card; } async function loadSubmissions() { const allowed = await isCommandAuthenticated(); if (!allowed) { locked.style.display = 'block'; locked.innerHTML = `
    ACCESS RESTRICTED

    EXAM REVIEW ACCESS REQUIRED

    Your command account does not have permission to review exams.

    `; content.style.display = 'none'; return; } locked.style.display = 'none'; content.style.display = 'block'; panel.innerHTML = ''; const { data } = await client .from('vmp_exam_submissions') .select('*') .order('submitted_at', { ascending: false }); if (!data || !data.length) { panel.innerHTML = '

    No exam submissions found.

    '; return; } data.forEach(item => { panel.appendChild(createCard(item)); }); } loadSubmissions(); } initExamReview(); })();
    ACCESS RESTRICTED

    COMMAND LOGIN REQUIRED

    You must be logged into the Command Portal to review police applications.

    VMP COMMAND

    DISCIPLINARY LOG

    Command-only officer conduct and disciplinary records.

    ADD RECORD

    SEARCH RECORDS
    Loading records...
    Loading disciplinary records...

    DELETE RECORD

    Are you sure you want to delete this disciplinary record?

    var DISC_URL = "https://aexslkhqwlgjfvqkdtqb.supabase.co"; var DISC_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFleHNsa2hxd2xnamZ2cWtkdHFiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg4Nzk5MDMsImV4cCI6MjA5NDQ1NTkwM30.neZVBV6tyjjZtkRYX4lmRn_MyFgdRTUQv3rNS8HwpBE"; var DISC_RECORDS = []; var pendingDeleteId = null; function getDiscEmail() { try { var raw = localStorage.getItem("sb-aexslkhqwlgjfvqkdtqb-auth-token"); if (raw) { var data = JSON.parse(raw); if (data.user && data.user.email) return data.user.email.toLowerCase().trim(); } } catch (e) {} try { for (var i = 0; i < localStorage.length; i++) { var value = localStorage.getItem(localStorage.key(i)); if (!value) continue; var match = String(value).match(/[A-Z0-9._%+-]+@vmpcommand\.co\.uk/i); if (match) return match[0].toLowerCase().trim(); try { var parsed = JSON.parse(value); var text = JSON.stringify(parsed); var parsedMatch = text.match(/[A-Z0-9._%+-]+@vmpcommand\.co\.uk/i); if (parsedMatch) return parsedMatch[0].toLowerCase().trim(); } catch (e2) {} } } catch (e3) {} try { for (var j = 0; j < sessionStorage.length; j++) { var sessionValue = sessionStorage.getItem(sessionStorage.key(j)); if (!sessionValue) continue; var sessionMatch = String(sessionValue).match(/[A-Z0-9._%+-]+@vmpcommand\.co\.uk/i); if (sessionMatch) return sessionMatch[0].toLowerCase().trim(); } } catch (e4) {} if (window.VMP_COMMAND_EMAIL) { return String(window.VMP_COMMAND_EMAIL).toLowerCase().trim(); } return ""; } async function isDiscCommand() { var email = getDiscEmail(); if (!email) return false; try { var response = await fetch( DISC_URL + "/rest/v1/vmp_terminal_permissions?select=id&email=eq." + encodeURIComponent(email) + "&module=eq.disciplinary", { headers: { "apikey": DISC_KEY, "Authorization": "Bearer " + DISC_KEY } } ); if (!response.ok) return false; var data = await response.json(); return Array.isArray(data) && data.length > 0; } catch (e) { console.error(e); return false; } } function getDiscDisplay(email) { if (!email) return "Unknown"; return email.split("@")[0]; } function severityClass(sev) { if (sev === "High") return "sev-high"; if (sev === "Medium") return "sev-medium"; return "sev-low"; } function actionTextClass(action) { if (action === "Warning") return "action-warning-text"; if (action === "Strike") return "action-strike-text"; if (action === "Suspension") return "action-suspension-text"; if (action === "Under Investigation") return "action-investigation-text"; return "action-note-text"; } function actionDotClass(action) { if (action === "Warning") return "action-warning-dot"; if (action === "Strike") return "action-strike-dot"; if (action === "Suspension") return "action-suspension-dot"; if (action === "Under Investigation") return "action-investigation-dot"; return "action-note-dot"; } function makeDiscRow(label, value, issued) { var row = document.createElement("div"); row.className = "vmp-disc-row"; if (issued) row.classList.add("vmp-disc-issued"); var l = document.createElement("span"); l.className = "vmp-disc-row-label"; l.textContent = label; var v = document.createElement("span"); v.className = "vmp-disc-row-value"; v.textContent = value || "N/A"; row.appendChild(l); row.appendChild(v); return row; } function recordMatchesSearch(r, query) { if (!query) return true; var joined = [ r.officer_name, r.callsign, r.rank, r.action_type, r.severity, r.outcome, r.reason, getDiscDisplay(r.issued_by) ].join(" ").toLowerCase(); return joined.indexOf(query) !== -1; } function renderDisciplinaryLog() { var box = document.getElementById("discResults"); var count = document.getElementById("discSearchCount"); var search = document.getElementById("discSearch"); if (!box) return; var query = search ? search.value.trim().toLowerCase() : ""; var filtered = DISC_RECORDS.filter(function(r) { return recordMatchesSearch(r, query); }); box.innerHTML = ""; if (count) { count.textContent = query ? "Showing " + filtered.length + " of " + DISC_RECORDS.length + " record(s) for: " + query : "Showing all " + DISC_RECORDS.length + " disciplinary record(s)."; } if (!filtered.length) { box.innerHTML = '
    No matching disciplinary records found.
    '; return; } filtered.forEach(function(r) { var card = document.createElement("div"); card.className = "vmp-disc-card"; var top = document.createElement("div"); top.className = "vmp-disc-top"; var left = document.createElement("div"); var name = document.createElement("div"); name.className = "vmp-disc-name"; name.textContent = r.officer_name || "Unknown Officer"; var actionLine = document.createElement("div"); actionLine.className = "vmp-disc-action-line " + actionTextClass(r.action_type); var dot = document.createElement("span"); dot.className = "vmp-disc-dot " + actionDotClass(r.action_type); var actionText = document.createElement("span"); actionText.textContent = r.action_type || "Disciplinary Record"; actionLine.appendChild(dot); actionLine.appendChild(actionText); left.appendChild(name); left.appendChild(actionLine); var badge = document.createElement("div"); badge.className = "vmp-disc-badge " + severityClass(r.severity); badge.textContent = r.severity || "Low"; top.appendChild(left); top.appendChild(badge); var details = document.createElement("div"); details.appendChild(makeDiscRow("Callsign", r.callsign)); details.appendChild(makeDiscRow("Rank", r.rank)); var outcomeRow = makeDiscRow("Outcome", r.outcome || "Pending Review"); outcomeRow.className = "vmp-disc-row vmp-disc-outcome"; details.appendChild(outcomeRow); details.appendChild(makeDiscRow("Issued By", getDiscDisplay(r.issued_by), true)); details.appendChild(makeDiscRow("Issued At", r.created_at ? new Date(r.created_at).toLocaleString("en-GB") : "N/A", true)); var reason = document.createElement("div"); reason.className = "vmp-disc-reason"; var title = document.createElement("div"); title.className = "vmp-disc-reason-title"; title.textContent = "Details"; var text = document.createElement("div"); text.className = "vmp-disc-reason-text"; text.textContent = r.reason || "No details provided."; reason.appendChild(title); reason.appendChild(text); var del = document.createElement("button"); del.className = "vmp-disc-delete"; del.textContent = "DELETE RECORD"; del.onclick = function() { deleteDiscRecord(r.id); }; card.appendChild(top); card.appendChild(details); card.appendChild(reason); card.appendChild(del); box.appendChild(card); }); } async function loadDisciplinaryLog() { var box = document.getElementById("discResults"); var count = document.getElementById("discSearchCount"); if (!box) return; var allowed = await isDiscCommand(); if (!allowed) { box.innerHTML = '
    Access denied. Disciplinary permission required.
    '; if (count) count.textContent = "Disciplinary permission required."; return; } fetch(DISC_URL + "/rest/v1/vmp_disciplinary_log?select=*&order=created_at.desc", { headers: { "apikey": DISC_KEY, "Authorization": "Bearer " + DISC_KEY } }) .then(function(r) { return r.json(); }) .then(function(data) { DISC_RECORDS = data || []; renderDisciplinaryLog(); }) .catch(function(err) { console.error(err); box.innerHTML = '
    Failed to load disciplinary records.
    '; if (count) count.textContent = "Failed to load records."; }); } async function addDiscRecord() { var msg = document.getElementById("discMessage"); var allowed = await isDiscCommand(); if (!allowed) { msg.style.color = "#ff7d9c"; msg.textContent = "Access denied."; return; } var officer = document.getElementById("discOfficer").value.trim(); var callsign = document.getElementById("discCallsign").value.trim(); var rank = document.getElementById("discRank").value.trim(); var action = document.getElementById("discAction").value; var severity = document.getElementById("discSeverity").value; var outcome = document.getElementById("discOutcome").value; var reason = document.getElementById("discReason").value.trim(); if (!officer || !callsign || !rank || !action || !severity || !outcome || !reason) { msg.style.color = "#ff7d9c"; msg.textContent = "Please complete all fields."; return; } msg.style.color = "#22E3E3"; msg.textContent = "Adding disciplinary record..."; fetch(DISC_URL + "/rest/v1/vmp_disciplinary_log", { method: "POST", headers: { "apikey": DISC_KEY, "Authorization": "Bearer " + DISC_KEY, "Content-Type": "application/json", "Prefer": "return=minimal" }, body: JSON.stringify({ officer_name: officer, callsign: callsign, rank: rank, action_type: action, severity: severity, outcome: outcome, reason: reason, issued_by: getDiscEmail() }) }).then(function(res) { if (!res.ok) { return res.text().then(function(txt) { msg.style.color = "#ff7d9c"; msg.textContent = "Failed: " + txt; }); } document.getElementById("discOfficer").value = ""; document.getElementById("discCallsign").value = ""; document.getElementById("discRank").value = ""; document.getElementById("discAction").value = ""; document.getElementById("discSeverity").value = ""; document.getElementById("discOutcome").value = ""; document.getElementById("discReason").value = ""; msg.style.color = "#22E3E3"; msg.textContent = "Disciplinary record added."; loadDisciplinaryLog(); }).catch(function(err) { console.error(err); msg.style.color = "#ff7d9c"; msg.textContent = "Failed to add disciplinary record."; }); } function deleteDiscRecord(id) { pendingDeleteId = id; var modal = document.getElementById("vmpDeleteModal"); if (modal) modal.style.display = "flex"; } async function confirmDiscDelete() { if (!pendingDeleteId) return; var allowed = await isDiscCommand(); if (!allowed) { pendingDeleteId = null; var deniedModal = document.getElementById("vmpDeleteModal"); if (deniedModal) deniedModal.style.display = "none"; alert("Access denied."); return; } fetch(DISC_URL + "/rest/v1/vmp_disciplinary_log?id=eq." + pendingDeleteId, { method: "DELETE", headers: { "apikey": DISC_KEY, "Authorization": "Bearer " + DISC_KEY } }).then(function() { pendingDeleteId = null; document.getElementById("vmpDeleteModal").style.display = "none"; loadDisciplinaryLog(); }); } function cancelDiscDelete() { pendingDeleteId = null; var modal = document.getElementById("vmpDeleteModal"); if (modal) modal.style.display = "none"; } setTimeout(function() { var btn = document.getElementById("discSubmitBtn"); var search = document.getElementById("discSearch"); var clear = document.getElementById("discClearSearch"); var confirmBtn = document.getElementById("vmpConfirmDelete"); var cancelBtn = document.getElementById("vmpCancelDelete"); if (btn) btn.onclick = addDiscRecord; if (search) { search.addEventListener("input", renderDisciplinaryLog); } if (clear) { clear.onclick = function() { document.getElementById("discSearch").value = ""; renderDisciplinaryLog(); }; } if (confirmBtn) confirmBtn.onclick = confirmDiscDelete; if (cancelBtn) cancelBtn.onclick = cancelDiscDelete; loadDisciplinaryLog(); }, 500);
    VMP BOOKING SYSTEM

    TRAINING BOOKING

    View available training sessions, submit a booking request, and check your private booking outcome.

    CHECK BOOKING STATUS

    Enter your private booking reference to view your training booking outcome.

    No booking reference checked yet.

    AVAILABLE SESSIONS

    Loading training sessions...

    Loading training sessions...
    (function () { const TB_URL = "https://aexslkhqwlgjfvqkdtqb.supabase.co"; const TB_KEY = "sb_publishable_OyUN0atXOWXCfw6svCVpHA_7flXAiqA"; let TRAINING_SESSIONS = []; function make(el, cls, text) { const node = document.createElement(el); if (cls) node.className = cls; if (text) node.textContent = text; return node; } function row(label, value) { const div = make("div", "vmp-tb-row"); div.appendChild(make("div", "vmp-tb-label", label)); div.appendChild(make("div", "vmp-tb-value", value || "N/A")); return div; } function makeBookingRef() { const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; let code = ""; for (let i = 0; i < 6; i++) code += chars.charAt(Math.floor(Math.random() * chars.length)); return "VMP-TB-" + code; } function prettyDate(value) { if (!value) return "N/A"; try { return new Date(value).toLocaleString("en-GB"); } catch (e) { return value; } } function shortName(email) { return email ? String(email).split("@")[0] : "Command"; } function bookingStatusText(b) { if (b.outcome === "PASSED") return "TRAINING PASSED"; if (b.outcome === "FAILED") return "TRAINING FAILED"; return b.status || "PENDING REVIEW"; } function bookingStatusClass(b) { if (b.outcome === "PASSED") return "tb-passed"; if (b.outcome === "FAILED") return "tb-failed"; if (b.status === "TRAINING IN PROGRESS") return "tb-progress"; if (b.status === "DENIED") return "tb-denied"; if (b.status === "ACCEPTED") return "tb-accepted"; return "tb-pending"; } function statusClass(session, booked, max) { if (session.status !== "OPEN") return "tb-closed"; if (booked >= max) return "tb-full"; return "tb-open"; } function statusText(session, booked, max) { if (session.status !== "OPEN") return "CLOSED"; if (booked >= max) return "FULL"; return "OPEN"; } function matches(session, q) { if (!q) return true; return [session.title, session.training_type, session.instructor, session.training_date, session.training_time, session.notes, session.status].join(" ").toLowerCase().includes(q); } function renderBookingResult(b) { const session = b.vmp_training_sessions || {}; const card = make("div", "vmp-tb-result-card"); const top = make("div", "vmp-tb-top"); const left = make("div"); left.appendChild(make("div", "vmp-tb-title", b.officer_name || "Unknown Officer")); left.appendChild(make("div", "vmp-tb-type", session.title || "Training Booking")); const badge = make("div", "vmp-tb-status " + bookingStatusClass(b), bookingStatusText(b)); top.appendChild(left); top.appendChild(badge); card.appendChild(top); card.appendChild(row("Reference", b.booking_ref)); card.appendChild(row("Callsign", b.callsign)); card.appendChild(row("Rank", b.rank)); card.appendChild(row("Training Type", session.training_type)); card.appendChild(row("Instructor", session.instructor)); card.appendChild(row("Date", session.training_date)); card.appendChild(row("Time", session.training_time)); card.appendChild(row("Booked At", prettyDate(b.created_at))); if (b.reviewed_by) { card.appendChild(row("Reviewed By", shortName(b.reviewed_by))); card.appendChild(row("Reviewed At", prettyDate(b.reviewed_at))); } if (b.outcome) { card.appendChild(row("Outcome", b.outcome)); card.appendChild(row("Outcome By", shortName(b.outcome_by))); card.appendChild(row("Outcome At", prettyDate(b.outcome_at))); } if (b.certificate_awarded) card.appendChild(row("Certificate / Role", b.certificate_awarded)); if (b.command_notes) card.appendChild(make("div", "vmp-tb-command-note", "Command Note: " + b.command_notes)); return card; } async function checkBookingRef() { const input = document.getElementById("tbBookingRefSearch"); const result = document.getElementById("tbBookingRefResult"); const ref = input.value.trim().toUpperCase(); if (!ref) { result.innerHTML = ""; result.appendChild(make("div", "vmp-tb-empty", "Enter your private booking reference first.")); return; } result.innerHTML = ""; result.appendChild(make("div", "vmp-tb-empty", "Checking booking reference...")); const res = await fetch(TB_URL + "/rest/v1/vmp_training_bookings?select=*,vmp_training_sessions(*)&booking_ref=eq." + encodeURIComponent(ref) + "&limit=1", { headers: { apikey: TB_KEY, Authorization: "Bearer " + TB_KEY } }); const rows = await res.json(); result.innerHTML = ""; if (!rows.length) { result.appendChild(make("div", "vmp-tb-empty", "No booking found for that reference.")); return; } result.appendChild(renderBookingResult(rows[0])); } async function loadTrainingSessions() { const list = document.getElementById("tbList"); const count = document.getElementById("tbCount"); list.innerHTML = ""; list.appendChild(make("div", "vmp-tb-empty", "Loading training sessions...")); const res = await fetch(TB_URL + "/rest/v1/vmp_training_sessions?select=*,vmp_training_bookings(*)&order=training_date.asc", { headers: { apikey: TB_KEY, Authorization: "Bearer " + TB_KEY } }); TRAINING_SESSIONS = await res.json(); renderTrainingSessions(); } function renderTrainingSessions() { const list = document.getElementById("tbList"); const count = document.getElementById("tbCount"); const search = document.getElementById("tbSearch"); const q = search ? search.value.trim().toLowerCase() : ""; const filtered = TRAINING_SESSIONS.filter(s => matches(s, q)); list.innerHTML = ""; count.textContent = "Showing all " + filtered.length + " training session(s)"; if (!filtered.length) { list.appendChild(make("div", "vmp-tb-empty", "No training sessions available.")); return; } filtered.forEach(function(session) { const bookings = session.vmp_training_bookings || []; const booked = bookings.filter(b => b.status === "ACCEPTED" || b.status === "TRAINING IN PROGRESS" || b.outcome === "PASSED").length; const max = Number(session.max_slots || 0); const canBook = session.status === "OPEN" && booked < max; const card = make("div", "vmp-tb-card"); const top = make("div", "vmp-tb-top"); const left = make("div"); left.appendChild(make("div", "vmp-tb-title", session.title || "Training Session")); left.appendChild(make("div", "vmp-tb-type", session.training_type || "Training")); const badge = make("div", "vmp-tb-status " + statusClass(session, booked, max), statusText(session, booked, max)); top.appendChild(left); top.appendChild(badge); card.appendChild(top); card.appendChild(row("Instructor", session.instructor)); card.appendChild(row("Date", session.training_date)); card.appendChild(row("Time", session.training_time)); card.appendChild(row("Slots", booked + " / " + max)); if (session.notes) card.appendChild(make("div", "vmp-tb-notes", session.notes)); const actions = make("div", "vmp-tb-actions"); const bookBtn = make("button", "vmp-tb-btn", canBook ? "BOOK TRAINING" : "UNAVAILABLE"); bookBtn.type = "button"; bookBtn.disabled = !canBook; actions.appendChild(bookBtn); card.appendChild(actions); const form = make("div", "vmp-tb-book-form"); form.appendChild(make("h4", "", "BOOK THIS TRAINING")); const officer = make("input", ""); officer.placeholder = "Officer name"; const callsign = make("input", ""); callsign.placeholder = "Callsign"; const rank = make("input", ""); rank.placeholder = "Rank"; const submit = make("button", "vmp-tb-btn", "SUBMIT BOOKING REQUEST"); submit.type = "button"; const status = make("p", "vmp-tb-form-status"); form.appendChild(officer); form.appendChild(callsign); form.appendChild(rank); form.appendChild(submit); form.appendChild(status); bookBtn.onclick = function() { form.classList.toggle("open"); }; submit.onclick = async function() { if (!officer.value.trim() || !callsign.value.trim() || !rank.value.trim()) { status.textContent = "Please complete all fields."; return; } const bookingRef = makeBookingRef(); status.textContent = "Submitting booking request..."; const response = await fetch(TB_URL + "/rest/v1/vmp_training_bookings", { method: "POST", headers: { apikey: TB_KEY, Authorization: "Bearer " + TB_KEY, "Content-Type": "application/json", Prefer: "return=minimal" }, body: JSON.stringify({ session_id: session.id, officer_name: officer.value.trim(), callsign: callsign.value.trim(), rank: rank.value.trim(), status: "PENDING REVIEW", booking_ref: bookingRef }) }); if (!response.ok) { status.textContent = "Failed to submit booking."; return; } officer.value = ""; callsign.value = ""; rank.value = ""; status.innerHTML = 'Booking request submitted. Save your private reference:' + bookingRef + ''; document.getElementById("tbBookingRefSearch").value = bookingRef; const result = document.getElementById("tbBookingRefResult"); result.innerHTML = ""; const refBox = make("div", "vmp-tb-ref-box"); refBox.innerHTML = "Your booking has been submitted. Keep this reference private:" + bookingRef + ""; result.appendChild(refBox); setTimeout(loadTrainingSessions, 800); }; card.appendChild(form); list.appendChild(card); }); } document.getElementById("tbSearch").addEventListener("input", renderTrainingSessions); document.getElementById("tbClearSearch").onclick = function() { document.getElementById("tbSearch").value = ""; renderTrainingSessions(); }; document.getElementById("tbCheckBookingRef").addEventListener("click", checkBookingRef); document.getElementById("tbBookingRefSearch").addEventListener("keydown", function(e) { if (e.key === "Enter") checkBookingRef(); }); loadTrainingSessions(); })();
    VMP COMMAND

    BOOKING REVIEW

    Create training sessions, review officer bookings, and record training outcomes.

    Checking command access...
    (function () { const BR_URL = "https://aexslkhqwlgjfvqkdtqb.supabase.co"; const BR_KEY = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImFleHNsa2hxd2xnamZ2cWtkdHFiIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Nzg4Nzk5MDMsImV4cCI6MjA5NDQ1NTkwM30.neZVBV6tyjjZtkRYX4lmRn_MyFgdRTUQv3rNS8HwpBE"; let BOOKINGS = []; function getCommandEmail() { try { for (let i = 0; i < localStorage.length; i++) { const value = localStorage.getItem(localStorage.key(i)); if (!value) continue; if (value.includes("@vmpcommand.co.uk")) { const match = value.match(/[A-Z0-9._%+-]+@vmpcommand\.co\.uk/i); if (match) return match[0].toLowerCase(); } try { const parsed = JSON.parse(value); if (parsed.user && parsed.user.email) return parsed.user.email.toLowerCase(); } catch (e) {} } } catch (e) {} return ""; } async function isCommand() { const email = getCommandEmail(); if (!email) return false; try { const response = await fetch( BR_URL + "/rest/v1/vmp_terminal_permissions?select=id&email=eq." + encodeURIComponent(email) + "&module=eq.bookings", { headers: { "apikey": BR_KEY, "Authorization": "Bearer " + BR_KEY } } ); const data = await response.json(); return Array.isArray(data) && data.length > 0; } catch (e) { console.error(e); return false; } } function shortName(email) { return email ? email.split("@")[0] : "Command"; } function brStatusClass(b) { if (b.outcome === "PASSED") return "br-passed"; if (b.outcome === "FAILED") return "br-failed"; if (b.status === "TRAINING IN PROGRESS") return "br-progress"; if (b.status === "DENIED") return "br-denied"; if (b.status === "ACCEPTED") return "br-accepted"; return "br-pending"; } function brStatusText(b) { if (b.outcome === "PASSED") return "TRAINING PASSED"; if (b.outcome === "FAILED") return "TRAINING FAILED"; return b.status || "PENDING REVIEW"; } function row(label, value, command) { const div = document.createElement("div"); div.className = "vmp-br-row"; const l = document.createElement("div"); l.className = "vmp-br-label"; l.textContent = label; const v = document.createElement("div"); v.className = "vmp-br-value"; if (command) v.classList.add("vmp-br-command"); v.textContent = value || "N/A"; div.appendChild(l); div.appendChild(v); return div; } function matches(b, q) { if (!q) return true; const s = b.vmp_training_sessions || {}; return [ b.officer_name, b.callsign, b.rank, b.status, b.outcome, b.certificate_awarded, s.title, s.training_type, s.instructor, s.training_date, s.training_time ].join(" ").toLowerCase().includes(q); } async function updateBooking(id, payload) { await fetch(BR_URL + "/rest/v1/vmp_training_bookings?id=eq." + id, { method: "PATCH", headers: { "apikey": BR_KEY, "Authorization": "Bearer " + BR_KEY, "Content-Type": "application/json", "Prefer": "return=minimal" }, body: JSON.stringify(payload) }); loadBookings(); } async function loadBookings() { const locked = document.getElementById("vmpBrLocked"); const content = document.getElementById("vmpBrContent"); if (!locked || !content) return; const allowed = await isCommand(); if (!allowed) { locked.style.display = "block"; locked.textContent = "Access denied. Bookings permission required."; content.style.display = "none"; return; } locked.style.display = "none"; content.style.display = "block"; const response = await fetch( BR_URL + "/rest/v1/vmp_training_bookings?select=*,vmp_training_sessions(*)&order=created_at.desc", { headers: { "apikey": BR_KEY, "Authorization": "Bearer " + BR_KEY } } ); BOOKINGS = await response.json(); renderBookings(); } function renderBookings() { const list = document.getElementById("brList"); const count = document.getElementById("brCount"); const search = document.getElementById("brSearch"); if (!list) return; const q = search ? search.value.trim().toLowerCase() : ""; const filtered = BOOKINGS.filter(b => matches(b, q)); list.innerHTML = ""; if (count) { count.textContent = q ? "Showing " + filtered.length + " of " + BOOKINGS.length + " booking(s)" : "Showing all " + BOOKINGS.length + " booking(s)"; } if (!filtered.length) { list.innerHTML = '
    No training bookings found.
    '; return; } filtered.forEach(function(b) { const session = b.vmp_training_sessions || {}; const card = document.createElement("div"); card.className = "vmp-br-card"; const top = document.createElement("div"); top.className = "vmp-br-top"; const left = document.createElement("div"); const name = document.createElement("div"); name.className = "vmp-br-name"; name.textContent = b.officer_name || "Unknown Officer"; const type = document.createElement("div"); type.className = "vmp-br-type"; type.textContent = session.title || "Training Booking"; left.appendChild(name); left.appendChild(type); const badge = document.createElement("div"); badge.className = "vmp-br-status " + brStatusClass(b); badge.textContent = brStatusText(b); top.appendChild(left); top.appendChild(badge); card.appendChild(top); card.appendChild(row("Callsign", b.callsign)); card.appendChild(row("Rank", b.rank)); card.appendChild(row("Training Type", session.training_type)); card.appendChild(row("Instructor", session.instructor)); card.appendChild(row("Date", session.training_date)); card.appendChild(row("Time", session.training_time)); card.appendChild(row("Booked At", b.created_at ? new Date(b.created_at).toLocaleString("en-GB") : "N/A")); if (b.reviewed_by) { card.appendChild(row("Reviewed By", shortName(b.reviewed_by), true)); card.appendChild(row("Reviewed At", b.reviewed_at ? new Date(b.reviewed_at).toLocaleString("en-GB") : "", true)); } if (b.outcome) { card.appendChild(row("Outcome", b.outcome, b.outcome === "PASSED")); card.appendChild(row("Outcome By", shortName(b.outcome_by), true)); card.appendChild(row("Outcome At", b.outcome_at ? new Date(b.outcome_at).toLocaleString("en-GB") : "", true)); } if (b.certificate_awarded) { card.appendChild(row("Certificate / Role", b.certificate_awarded, true)); } if (b.command_notes) { const note = document.createElement("div"); note.className = "vmp-br-note-box"; note.textContent = b.command_notes; card.appendChild(note); } const actions = document.createElement("div"); actions.className = "vmp-br-actions"; if (b.status === "PENDING REVIEW") { const accept = document.createElement("button"); accept.className = "vmp-br-btn accept"; accept.textContent = "ACCEPT BOOKING"; accept.onclick = function() { updateBooking(b.id, { status: "ACCEPTED", reviewed_by: getCommandEmail(), reviewed_at: new Date().toISOString() }); }; const deny = document.createElement("button"); deny.className = "vmp-br-btn deny"; deny.textContent = "DENY BOOKING"; deny.onclick = function() { updateBooking(b.id, { status: "DENIED", reviewed_by: getCommandEmail(), reviewed_at: new Date().toISOString() }); }; actions.appendChild(accept); actions.appendChild(deny); } if (b.status === "ACCEPTED" && !b.outcome) { const progress = document.createElement("button"); progress.className = "vmp-br-btn accept"; progress.textContent = "MARK IN PROGRESS"; progress.onclick = function() { updateBooking(b.id, { status: "TRAINING IN PROGRESS", reviewed_by: getCommandEmail(), reviewed_at: new Date().toISOString() }); }; actions.appendChild(progress); } if (b.status === "TRAINING IN PROGRESS" && !b.outcome) { const complete = document.createElement("div"); complete.className = "vmp-br-complete"; const cert = document.createElement("input"); cert.className = "vmp-br-cert"; cert.placeholder = "Certificate / role awarded if passed"; const note = document.createElement("textarea"); note.className = "vmp-br-note"; note.placeholder = "Optional command note"; const rowBtns = document.createElement("div"); rowBtns.className = "vmp-br-actions"; const pass = document.createElement("button"); pass.className = "vmp-br-btn pass"; pass.textContent = "MARK PASSED"; pass.onclick = function() { if (!cert.value.trim()) { alert("Please enter certificate / role awarded."); return; } updateBooking(b.id, { outcome: "PASSED", outcome_by: getCommandEmail(), outcome_at: new Date().toISOString(), certificate_awarded: cert.value.trim(), command_notes: note.value.trim() }); }; const fail = document.createElement("button"); fail.className = "vmp-br-btn fail"; fail.textContent = "MARK FAILED"; fail.onclick = function() { updateBooking(b.id, { outcome: "FAILED", outcome_by: getCommandEmail(), outcome_at: new Date().toISOString(), command_notes: note.value.trim() }); }; rowBtns.appendChild(pass); rowBtns.appendChild(fail); complete.appendChild(cert); complete.appendChild(note); complete.appendChild(rowBtns); card.appendChild(complete); } card.appendChild(actions); list.appendChild(card); }); } const createForm = document.getElementById("vmpCreateSessionForm"); if (createForm) { createForm.onsubmit = async function(e) { e.preventDefault(); const status = document.getElementById("brCreateStatus"); if (status) status.textContent = "Creating session..."; await fetch(BR_URL + "/rest/v1/vmp_training_sessions", { method: "POST", headers: { "apikey": BR_KEY, "Authorization": "Bearer " + BR_KEY, "Content-Type": "application/json", "Prefer": "return=minimal" }, body: JSON.stringify({ title: document.getElementById("brTitle").value.trim(), training_type: document.getElementById("brType").value, instructor: document.getElementById("brInstructor").value.trim(), training_date: document.getElementById("brDate").value, training_time: document.getElementById("brTime").value.trim(), max_slots: Number(document.getElementById("brSlots").value || 6), notes: document.getElementById("brNotes").value.trim(), status: "OPEN", created_by: getCommandEmail() }) }); createForm.reset(); document.getElementById("brSlots").value = 6; if (status) status.textContent = "Training session created."; setTimeout(function() { if (status) status.textContent = ""; }, 3500); loadBookings(); }; } const search = document.getElementById("brSearch"); if (search) search.addEventListener("input", renderBookings); const clear = document.getElementById("brClear"); if (clear) { clear.onclick = function() { document.getElementById("brSearch").value = ""; renderBookings(); }; } loadBookings(); })();
    VMP POLICE SERVICE

    PROMOTION LOG

    Celebrate officer achievements and service milestones.

    Loading promotions...
    setTimeout(function () { var URL = "https://aexslkhqwlgjfvqkdtqb.supabase.co"; var KEY = "sb_publishable_OyUN0atXOWXCfw6svCVpHA_7flXAiqA"; var list = document.getElementById("vmpPlList"); var search = document.getElementById("vmpPlSearch"); var clear = document.getElementById("vmpPlClear"); var promotions = []; if (!list) return; function esc(v) { return String(v || "") .replace(/&/g, "&") .replace(//g, ">"); } function row(label, value) { return '
    ' + esc(label) + '
    ' + esc(value || "N/A") + '
    '; } function render() { var q = search ? search.value.trim().toLowerCase() : ""; var filtered = promotions.filter(function (p) { return [ p.officer_name, p.callsign, p.previous_rank, p.old_rank, p.new_rank, p.action_type, p.reason ].join(" ").toLowerCase().includes(q); }); if (!filtered.length) { list.innerHTML = '
    No promotions found.
    '; return; } list.innerHTML = filtered.map(function (p) { var oldRank = p.previous_rank || p.old_rank || "N/A"; var newRank = p.new_rank || "N/A"; return '' + '
    ' + '
    ' + '
    ' + '
    ' + esc(p.officer_name || "Unknown Officer") + '
    ' + '
    ' + esc(p.action_type || "PROMOTION") + '
    ' + '
    ' + '
    ' + esc(oldRank) + ' → ' + esc(newRank) + '
    ' + '
    ' + row("Callsign", p.callsign) + row("Date", p.created_at ? new Date(p.created_at).toLocaleString("en-GB") : "N/A") + '
    ' + esc(p.reason || "No reason provided.") + '
    ' + '
    '; }).join(""); } list.innerHTML = '
    Connecting to promotions...
    '; fetch(URL + "/rest/v1/vmp_promotions?select=*&order=created_at.desc", { headers: { "apikey": KEY, "Authorization": "Bearer " + KEY } }) .then(function (res) { if (!res.ok) { return res.text().then(function (txt) { list.innerHTML = '
    Failed to load: ' + esc(txt) + '
    '; }); } return res.json(); }) .then(function (data) { if (!data) return; promotions = data; render(); }) .catch(function (err) { list.innerHTML = '
    Connection error.
    '; console.error(err); }); if (search) search.addEventListener("input", render); if (clear) { clear.onclick = function () { if (search) search.value = ""; render(); }; } }, 1000);
    setTimeout(function () { var URL = "https://aexslkhqwlgjfvqkdtqb.supabase.co"; var KEY = "sb_publishable_OyUN0atXOWXCfw6svCVpHA_7flXAiqA"; var list = document.getElementById("vmpPlList"); var search = document.getElementById("vmpPlSearch"); var clear = document.getElementById("vmpPlClear"); var promotions = []; if (!list) return; var reactorId = localStorage.getItem("vmp_promo_reactor"); if (!reactorId) { reactorId = String(Date.now()) + "_" + Math.random(); localStorage.setItem("vmp_promo_reactor", reactorId); } function make(tag, cls, text) { var el = document.createElement(tag); if (cls) el.className = cls; if (text !== undefined && text !== null) el.textContent = text; return el; } function row(label, value) { var r = make("div", "vmp-pl-row"); r.appendChild(make("div", "vmp-pl-label", label)); r.appendChild(make("div", "vmp-pl-value", value || "N/A")); return r; } function hasReacted(p) { return (p.vmp_promotion_reactions || []).some(function (r) { return r.reactor_id === reactorId; }); } function render() { var q = search ? search.value.trim().toLowerCase() : ""; var filtered = promotions.filter(function (p) { return [ p.officer_name, p.callsign, p.previous_rank, p.old_rank, p.new_rank, p.action_type, p.reason ].join(" ").toLowerCase().includes(q); }); list.innerHTML = ""; if (!filtered.length) { list.appendChild(make("div", "vmp-pl-empty", "No promotions found.")); return; } filtered.forEach(function (p) { var oldRank = p.previous_rank || p.old_rank || "N/A"; var newRank = p.new_rank || "N/A"; var count = (p.vmp_promotion_reactions || []).length; var active = hasReacted(p); var card = make("div", "vmp-pl-card"); var top = make("div", "vmp-pl-top"); var left = make("div"); left.appendChild(make("div", "vmp-pl-name", p.officer_name || "Unknown Officer")); left.appendChild(make("div", "vmp-pl-action", p.action_type || "PROMOTION")); top.appendChild(left); top.appendChild(make("div", "vmp-pl-badge", oldRank + " → " + newRank)); card.appendChild(top); card.appendChild(row("Callsign", p.callsign)); card.appendChild(row("Date", p.created_at ? new Date(p.created_at).toLocaleString("en-GB") : "N/A")); card.appendChild(make("div", "vmp-pl-reason", p.reason || "No reason provided.")); var btn = make("button", "vmp-pl-react" + (active ? " active" : ""), "👏 " + count); btn.type = "button"; btn.onclick = function () { toggleReaction(p.id, active); }; card.appendChild(btn); list.appendChild(card); }); } function loadPromotions() { list.innerHTML = ""; list.appendChild(make("div", "vmp-pl-empty", "Loading promotions...")); fetch(URL + "/rest/v1/vmp_promotions?select=*,vmp_promotion_reactions(*)&order=created_at.desc", { headers: { "apikey": KEY, "Authorization": "Bearer " + KEY } }) .then(function (res) { if (!res.ok) { return res.text().then(function (txt) { list.innerHTML = ""; list.appendChild(make("div", "vmp-pl-empty", "Failed to load promotions: " + txt)); }); } return res.json(); }) .then(function (data) { if (!data) return; promotions = data; render(); }) .catch(function () { list.innerHTML = ""; list.appendChild(make("div", "vmp-pl-empty", "Connection error loading promotions.")); }); } function toggleReaction(id, active) { if (active) { fetch(URL + "/rest/v1/vmp_promotion_reactions?promotion_id=eq." + id + "&reactor_id=eq." + reactorId, { method: "DELETE", headers: { "apikey": KEY, "Authorization": "Bearer " + KEY } }).then(loadPromotions); } else { fetch(URL + "/rest/v1/vmp_promotion_reactions", { method: "POST", headers: { "apikey": KEY, "Authorization": "Bearer " + KEY, "Content-Type": "application/json", "Prefer": "return=minimal" }, body: JSON.stringify({ promotion_id: id, reaction_type: "CLAP", reactor_id: reactorId }) }).then(loadPromotions); } } if (search) search.addEventListener("input", render); if (clear) { clear.onclick = function () { if (search) search.value = ""; render(); }; } loadPromotions(); }, 800);
    VMP COMMAND

    PROMOTION MANAGER

    Add and manage promotion records shown on the public promotion log.

    Checking command access...
    setTimeout(function () { var URL = "https://aexslkhqwlgjfvqkdtqb.supabase.co"; var KEY = "sb_publishable_OyUN0atXOWXCfw6svCVpHA_7flXAiqA"; var locked = document.getElementById("vmpPmLocked"); var content = document.getElementById("vmpPmContent"); var form = document.getElementById("vmpPmForm"); function findEmail(value) { if (!value) return ""; var text = String(value); try { text += " " + JSON.stringify(JSON.parse(value)); } catch (e) {} var match = text.match(/[A-Z0-9._%+-]+@vmpcommand\.co\.uk/i); return match ? match[0].toLowerCase().trim() : ""; } function getCommandEmail() { var keys = [ "vmp_command_email", "vmpCommandEmail", "vmp_command_user", "vmpCommandUser", "vmp_logged_email", "vmpLoggedEmail", "command_email", "commandEmail", "email" ]; for (var i = 0; i < keys.length; i++) { var foundLocal = findEmail(localStorage.getItem(keys[i])); if (foundLocal) return foundLocal; var foundSession = findEmail(sessionStorage.getItem(keys[i])); if (foundSession) return foundSession; } for (var j = 0; j < localStorage.length; j++) { var foundAnyLocal = findEmail(localStorage.getItem(localStorage.key(j))); if (foundAnyLocal) return foundAnyLocal; } for (var k = 0; k < sessionStorage.length; k++) { var foundAnySession = findEmail(sessionStorage.getItem(sessionStorage.key(k))); if (foundAnySession) return foundAnySession; } if (window.VMP_COMMAND_EMAIL) { return String(window.VMP_COMMAND_EMAIL).toLowerCase().trim(); } return ""; } function showDenied() { locked.style.display = "block"; locked.textContent = "Access denied. Promotions permission required."; content.style.display = "none"; } function showAllowed() { locked.style.display = "none"; content.style.display = "block"; var toolbar = document.querySelector("#vmp-promotion-manager .vmp-pm-toolbar"); var list = document.getElementById("pmList"); if (toolbar) toolbar.style.display = "none"; if (list) list.style.display = "none"; } function checkPromotionPermission(email) { return fetch( URL + "/rest/v1/vmp_terminal_permissions?select=id&email=eq." + encodeURIComponent(email) + "&module=eq.promotions", { method: "GET", headers: { "apikey": KEY, "Authorization": "Bearer " + KEY, "Content-Type": "application/json" } } ) .then(function (res) { if (!res.ok) return false; return res.json().then(function (data) { return Array.isArray(data) && data.length > 0; }); }) .catch(function () { return false; }); } var email = getCommandEmail(); console.log("VMP Promotion Manager detected email:", email); if (!email) { showDenied(); return; } checkPromotionPermission(email).then(function (allowed) { if (!allowed) { showDenied(); return; } showAllowed(); form.addEventListener("submit", function (e) { e.preventDefault(); var status = document.getElementById("pmStatus"); status.textContent = "Adding promotion record..."; var officer = document.getElementById("pmOfficer").value.trim(); var callsign = document.getElementById("pmCallsign").value.trim(); var previousRank = document.getElementById("pmPreviousRank").value.trim(); var newRank = document.getElementById("pmNewRank").value.trim(); var action = document.getElementById("pmAction").value; var reason = document.getElementById("pmReason").value.trim(); if (!officer || !previousRank || !newRank || !reason) { status.textContent = "Please complete all required fields."; return; } var payload = { officer_name: officer, callsign: callsign, previous_rank: previousRank, new_rank: newRank, action_type: action, reason: reason, issued_by: email, promoted_by: email, old_rank: previousRank }; fetch(URL + "/rest/v1/vmp_promotions", { method: "POST", headers: { "apikey": KEY, "Authorization": "Bearer " + KEY, "Content-Type": "application/json", "Prefer": "return=minimal" }, body: JSON.stringify(payload) }).then(function (res) { if (!res.ok) { return res.text().then(function (txt) { status.textContent = "Failed: " + txt; }); } form.reset(); status.textContent = "Promotion record added. It will now show on the public promotion log."; }).catch(function (err) { status.textContent = "Failed to add promotion record."; console.error(err); }); }); }); }, 500);
    VMP COMMAND

    PERMISSIONS MANAGER

    Manage which command emails can access each terminal module.

    Checking permissions...
    (function () { function startPermissionsManager() { if (!window.supabase) { setTimeout(startPermissionsManager, 300); return; } var URL = "https://aexslkhqwlgjfvqkdtqb.supabase.co"; var KEY = "sb_publishable_OyUN0atXOWXCfw6svCVpHA_7flXAiqA"; var client = window.supabase.createClient(URL, KEY); var locked = document.getElementById("vmpPermLocked"); var content = document.getElementById("vmpPermContent"); var list = document.getElementById("permList"); var status = document.getElementById("permStatus"); var addBtn = document.getElementById("addPerm"); var refreshBtn = document.getElementById("refreshPerms"); function findEmail(value) { if (!value) return ""; var text = String(value); try { text += " " + JSON.stringify(JSON.parse(value)); } catch (e) {} var match = text.match(/[A-Z0-9._%+-]+@vmpcommand\.co\.uk/i); return match ? match[0].toLowerCase().trim() : ""; } function getCommandEmail() { var keys = [ "vmp_command_email", "vmpCommandEmail", "vmp_command_user", "vmpCommandUser", "vmp_logged_email", "vmpLoggedEmail", "command_email", "commandEmail", "email" ]; for (var i = 0; i < keys.length; i++) { var foundLocal = findEmail(localStorage.getItem(keys[i])); if (foundLocal) return foundLocal; var foundSession = findEmail(sessionStorage.getItem(keys[i])); if (foundSession) return foundSession; } for (var j = 0; j < localStorage.length; j++) { var foundAnyLocal = findEmail(localStorage.getItem(localStorage.key(j))); if (foundAnyLocal) return foundAnyLocal; } for (var k = 0; k < sessionStorage.length; k++) { var foundAnySession = findEmail(sessionStorage.getItem(sessionStorage.key(k))); if (foundAnySession) return foundAnySession; } if (window.VMP_COMMAND_EMAIL) { return String(window.VMP_COMMAND_EMAIL).toLowerCase().trim(); } return ""; } function moduleLabel(moduleName) { var labels = { applications: "Application Review", suggestions: "Suggestions Manager", promotions: "Promotion Manager", bookings: "Booking Manager", exams: "Exam Review", loa: "LOA Review", disciplinary: "Disciplinary Log", permissions: "Permissions Manager" }; return labels[moduleName] || moduleName; } async function hasPermission(email, moduleName) { if (!email) return false; var result = await client .from("vmp_terminal_permissions") .select("id") .eq("email", email.toLowerCase().trim()) .eq("module", moduleName) .maybeSingle(); if (result.error) { console.error("Permission check failed:", result.error); return false; } return !!result.data; } async function loadPermissions() { list.innerHTML = ""; var loading = document.createElement("div"); loading.className = "perm-empty"; loading.textContent = "Loading permissions..."; list.appendChild(loading); var result = await client .from("vmp_terminal_permissions") .select("*") .order("email", { ascending: true }) .order("module", { ascending: true }); if (result.error) { console.error(result.error); list.innerHTML = ""; var failed = document.createElement("div"); failed.className = "perm-empty"; failed.textContent = "Failed to load permissions."; list.appendChild(failed); return; } list.innerHTML = ""; if (!result.data || !result.data.length) { var empty = document.createElement("div"); empty.className = "perm-empty"; empty.textContent = "No permissions found."; list.appendChild(empty); return; } result.data.forEach(function (item) { var row = document.createElement("div"); row.className = "perm-row"; var left = document.createElement("div"); var emailStrong = document.createElement("strong"); emailStrong.textContent = item.email || "Unknown email"; var moduleSpan = document.createElement("span"); moduleSpan.textContent = moduleLabel(item.module); var addedBy = document.createElement("small"); addedBy.textContent = "Added by: " + (item.created_by || "Unknown"); var removeBtn = document.createElement("button"); removeBtn.type = "button"; removeBtn.textContent = "REMOVE"; removeBtn.onclick = async function () { var confirmed = confirm("Remove " + moduleLabel(item.module) + " access for " + item.email + "?"); if (!confirmed) return; status.textContent = "Removing permission..."; var deleteResult = await client .from("vmp_terminal_permissions") .delete() .eq("id", item.id); if (deleteResult.error) { console.error(deleteResult.error); status.textContent = "Failed to remove permission."; return; } status.textContent = "Permission removed."; loadPermissions(); }; left.appendChild(emailStrong); left.appendChild(moduleSpan); left.appendChild(addedBy); row.appendChild(left); row.appendChild(removeBtn); list.appendChild(row); }); } async function init() { var email = getCommandEmail(); console.log("VMP Permissions Manager detected email:", email); var allowed = await hasPermission(email, "permissions"); if (!allowed) { locked.style.display = "block"; locked.textContent = "Access denied. Chief officers only."; content.style.display = "none"; return; } locked.style.display = "none"; content.style.display = "block"; loadPermissions(); } addBtn.onclick = async function () { var email = document.getElementById("permEmail").value.trim().toLowerCase(); var moduleName = document.getElementById("permModule").value; if (!email || email.indexOf("@") === -1) { status.textContent = "Enter a valid command email."; return; } if (!email.endsWith("@vmpcommand.co.uk")) { status.textContent = "Only @vmpcommand.co.uk emails can be added."; return; } var currentUser = getCommandEmail(); status.textContent = "Adding permission..."; addBtn.disabled = true; addBtn.textContent = "ADDING..."; var insertResult = await client .from("vmp_terminal_permissions") .insert({ email: email, module: moduleName, created_by: currentUser || "unknown" }); addBtn.disabled = false; addBtn.textContent = "ADD PERMISSION"; if (insertResult.error) { console.error(insertResult.error); status.textContent = "Failed or permission already exists."; return; } document.getElementById("permEmail").value = ""; status.textContent = "Permission added."; loadPermissions(); }; refreshBtn.onclick = function () { loadPermissions(); }; init(); } startPermissionsManager(); })();