wsj-filenaming/builder.php
DJP c9379fa89f Update TechLive events per client feedback
- Tech Live: TECL → TL
- TechLive Qatar: added (TLQ)
- TechLive Cyber: added (TLCYB)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 13:14:58 -05:00

703 lines
30 KiB
PHP

<?php require_once __DIR__ . '/auth.php'; requireAuth(); ?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Job Name Builder - Dow Jones</title>
<link rel="stylesheet" href="style.css">
<style>
.builder-container {
max-width: 960px;
margin: 0 auto;
padding: 20px;
}
.back-link {
display: inline-block;
margin-bottom: 20px;
color: var(--text-primary);
text-decoration: none;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.85rem;
border: 2px solid var(--text-primary);
padding: 8px 16px;
}
.back-link:hover {
background: var(--text-primary);
color: var(--bg-color);
}
/* Tabs */
.tabs {
display: flex;
gap: 0;
margin-bottom: 30px;
border-bottom: 3px solid var(--text-primary);
}
.tab {
padding: 12px 30px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.9rem;
cursor: pointer;
background: var(--card-bg);
border: 2px solid var(--border-color);
border-bottom: none;
color: var(--text-secondary);
transition: all 0.2s;
font-family: var(--font-sans);
}
.tab:hover {
background: #eee;
}
.tab.active {
background: var(--text-primary);
color: var(--bg-color);
border-color: var(--text-primary);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
/* Form */
.form-card {
background: var(--card-bg);
padding: 30px;
border-radius: 8px;
border: 1px solid var(--border-color);
margin-bottom: 25px;
}
.form-row {
display: flex;
gap: 20px;
margin-bottom: 18px;
align-items: flex-end;
}
.form-group {
flex: 1;
min-width: 0;
}
.form-group label {
display: block;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 0.75rem;
margin-bottom: 6px;
color: var(--text-secondary);
}
.form-group input,
.form-group select {
width: 100%;
padding: 10px 12px;
border: 2px solid var(--border-color);
border-radius: 4px;
font-size: 0.95rem;
font-family: var(--font-sans);
background: var(--bg-color);
color: var(--text-primary);
box-sizing: border-box;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--text-primary);
}
.form-group.hidden {
display: none;
}
/* Preview */
.preview-card {
background: #0f172a;
padding: 25px;
border-radius: 8px;
margin-bottom: 25px;
border: 1px solid var(--border-color);
}
.preview-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #94a3b8;
margin-bottom: 8px;
font-weight: 700;
}
.preview-filename {
font-family: 'Courier New', monospace;
font-size: 1.4rem;
color: #00ff88;
word-break: break-all;
min-height: 1.6em;
}
/* Decode */
.decode-input-group {
display: flex;
gap: 10px;
margin-bottom: 25px;
}
.decode-input-group input {
flex: 1;
padding: 14px 16px;
border: 2px solid var(--text-primary);
font-size: 1rem;
font-family: 'Courier New', monospace;
background: var(--bg-color);
color: var(--text-primary);
}
.decode-input-group input:focus {
outline: none;
border-color: var(--accent-color);
}
/* Decoded breakdown */
.breakdown {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 12px;
margin-bottom: 25px;
}
.breakdown-item {
background: var(--bg-color);
border: 1px solid var(--border-color);
padding: 12px;
border-radius: 4px;
}
.breakdown-item .label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-secondary);
font-weight: 700;
margin-bottom: 4px;
}
.breakdown-item .value {
font-size: 1.1rem;
font-weight: 600;
color: var(--text-primary);
font-family: 'Courier New', monospace;
}
.breakdown-item .full-name {
font-size: 0.8rem;
color: var(--text-secondary);
margin-top: 2px;
}
.action-row {
display: flex;
gap: 10px;
margin-top: 20px;
}
</style>
</head>
<body>
<div class="builder-container">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<a href="index.php" class="back-link" style="margin-bottom: 0;">Back to Naming Tool</a>
<div style="display: flex; gap: 10px; align-items: center;">
<span style="font-size: 0.8rem; color: var(--text-secondary);"><?php echo htmlspecialchars($CURRENT_USER); ?></span>
<a href="logout.php" class="btn btn-secondary" style="text-decoration: none;">Sign Out</a>
</div>
</div>
<header style="margin-bottom: 30px;">
<h1>JOB NAME BUILDER</h1>
<p style="color: var(--text-secondary);">Build or decode a single Dow Jones job name</p>
</header>
<!-- Tabs -->
<div class="tabs">
<div class="tab active" data-tab="build">Build</div>
<div class="tab" data-tab="decode">Decode</div>
</div>
<!-- BUILD TAB -->
<div id="tab-build" class="tab-content active">
<div class="form-card">
<div class="form-row">
<div class="form-group" style="flex: 0.7;">
<label>OMGID</label>
<input type="text" id="b-omgid" placeholder="000000" maxlength="6">
</div>
<div class="form-group">
<label>Domain</label>
<select id="b-domain">
<option value="">-- Select --</option>
<option value="PMKT">PMKT - Performance Marketing</option>
<option value="BRND">BRND - Brand</option>
<option value="EVNT">EVNT - Event</option>
<option value="B2B">B2B</option>
</select>
</div>
</div>
<div class="form-row" id="row-subteam-brand">
<div class="form-group" id="fg-subteam">
<label>Subteam</label>
<select id="b-subteam">
<option value="">-- Select --</option>
<option value="ACQ">ACQ - Acquisition</option>
<option value="B2B">B2B</option>
<option value="CMKT">CMKT - Content Marketing</option>
<option value="ENGRT">ENGRT - Engagement/Retention</option>
<option value="ENT">ENT - Enterprise</option>
</select>
</div>
<div class="form-group" id="fg-brand">
<label>Brand</label>
<select id="b-brand">
<option value="">-- Select --</option>
<option value="WSJ">WSJ - Wall Street Journal</option>
<option value="WSJ+">WSJ+</option>
<option value="BAR">BAR - Barron's</option>
<option value="MW">MW - MarketWatch</option>
<option value="DF">DF - Dragonfly</option>
<option value="DJE">DJE - Dow Jones Energy</option>
<option value="FAC">FAC - Factiva</option>
<option value="FE">FE - Free Expression</option>
<option value="GRI">GRI - Global Risk Insights</option>
<option value="NWS">NWS - Newswires</option>
<option value="OA">OA - Oxford Analytica</option>
<option value="RSK">RSK - Risk</option>
<option value="RSKC">RSKC - Risk Center</option>
<option value="DJRJ">DJRJ - Risk Journal</option>
<option value="R&C">R&amp;C - Risk &amp; Compliance</option>
<option value="WECR">WECR - World ECR</option>
</select>
</div>
</div>
<div class="form-row hidden" id="row-event">
<div class="form-group">
<label>Event</label>
<select id="b-event">
<option value="">-- Select --</option>
<option value="GH">GH - Global Horizons</option>
<option value="DJRJS">DJRJS - Risk Journal Summit</option>
<option value="WECR">WECR - World ECR</option>
<option value="FEOE">FEOE - Free Expressions Opinion Event</option>
<option value="FOH">FOH - Future of Health</option>
<option value="GFF">GFF - Global Food Forum</option>
<option value="JH">JH - Journal House</option>
<option value="TL">TL - TechLive</option>
<option value="TLQ">TLQ - TechLive Qatar</option>
<option value="TLCYB">TLCYB - TechLive Cyber</option>
<option value="FOE">FOE - The Future of Everything</option>
<option value="WSJIL">WSJIL - WSJ Invest Live</option>
<option value="BODC">BODC - Board of Directors Council</option>
<option value="CCOC">CCOC - CCO Council</option>
<option value="CEOC">CEOC - CEO Council</option>
<option value="CFOC">CFOC - CFO Council</option>
<option value="CMOC">CMOC - CMO Council</option>
<option value="CPOC">CPOC - CPO Council</option>
<option value="TECC">TECC - Technology Council</option>
<option value="WSJLI">WSJLI - WSJ Leadership Institute</option>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Initiative</label>
<input type="text" id="b-initiative" placeholder="e.g., BIC, DEMO">
</div>
<div class="form-group" style="flex: 0.4;">
<label>Year (YY)</label>
<input type="text" id="b-yy" placeholder="26" maxlength="2">
</div>
<div class="form-group" style="flex: 0.4;">
<label>Sequence</label>
<input type="text" id="b-seq" placeholder="01" maxlength="2">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label>Asset Name</label>
<input type="text" id="b-assetname" placeholder="e.g., MetaBanner, Email">
</div>
<div class="form-group" style="flex: 0.4;">
<label>Version</label>
<input type="text" id="b-version" placeholder="1" maxlength="3">
</div>
</div>
</div>
<!-- Live Preview -->
<div class="preview-card">
<div class="preview-label">Generated Job Name</div>
<div class="preview-filename" id="build-preview">--</div>
</div>
<div class="action-row">
<button class="btn btn-primary" id="pushBuildBtn">Push to Naming Tool</button>
<button class="btn btn-secondary" id="clearBuildBtn">Clear</button>
</div>
</div>
<!-- DECODE TAB -->
<div id="tab-decode" class="tab-content">
<div class="form-card">
<label style="display: block; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; font-size: 0.75rem; margin-bottom: 8px; color: var(--text-secondary);">Paste a job name to decode</label>
<div class="decode-input-group">
<input type="text" id="decode-input" placeholder="e.g., 000000 - PMKT-ACQ-WSJ-BIC-26-01_MetaBanner_v1">
<button class="btn btn-primary" id="decodeBtn">Decode</button>
</div>
<div id="decode-result" style="display: none;">
<div class="breakdown" id="decode-breakdown"></div>
<!-- Preview rebuilt -->
<div class="preview-card">
<div class="preview-label">Reconstructed Job Name</div>
<div class="preview-filename" id="decode-preview">--</div>
</div>
<div class="action-row">
<button class="btn btn-primary" id="pushDecodeBtn">Push to Naming Tool</button>
<button class="btn btn-secondary" id="editDecodeBtn">Edit in Builder</button>
</div>
</div>
<div id="decode-error" style="display: none; color: var(--danger-color); font-weight: 600; margin-top: 15px;"></div>
</div>
</div>
</div>
<script>
(function() {
// --- Lookup maps ---
const DOMAIN_NAMES = { PMKT: 'Performance Marketing', BRND: 'Brand', EVNT: 'Event', B2B: 'B2B' };
const SUBTEAM_NAMES = { ACQ: 'Acquisition', B2B: 'B2B', CMKT: 'Content Marketing', ENGRT: 'Engagement/Retention', ENT: 'Enterprise' };
const BRAND_NAMES = { WSJ: 'Wall Street Journal', 'WSJ+': 'Wall Street Journal+', BAR: "Barron's", MW: 'MarketWatch', DF: 'Dragonfly', DJE: 'Dow Jones Energy', FAC: 'Factiva', FE: 'Free Expression', GRI: 'Global Risk Insights', NWS: 'Newswires', OA: 'Oxford Analytica', RSK: 'Risk', RSKC: 'Risk Center', DJRJ: 'Risk Journal', 'R&C': 'Risk & Compliance', WECR: 'World ECR' };
const EVENT_NAMES = { GH: 'Global Horizons', DJRJS: 'Risk Journal Summit', WECR: 'World ECR', FEOE: 'Free Expressions Opinion Event', FOH: 'Future of Health', GFF: 'Global Food Forum', JH: 'Journal House', TL: 'TechLive', TLQ: 'TechLive Qatar', TLCYB: 'TechLive Cyber', FOE: 'The Future of Everything', WSJIL: 'WSJ Invest Live', BODC: 'Board of Directors Council', CCOC: 'CCO Council', CEOC: 'CEO Council', CFOC: 'CFO Council', CMOC: 'CMO Council', CPOC: 'CPO Council', TECC: 'Technology Council', WSJLI: 'WSJ Leadership Institute' };
const ALL_EVENTS = Object.keys(EVENT_NAMES);
const ALL_BRANDS = Object.keys(BRAND_NAMES);
const ALL_SUBTEAMS = Object.keys(SUBTEAM_NAMES);
// --- Tab switching ---
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
document.getElementById('tab-' + tab.dataset.tab).classList.add('active');
});
});
// --- Build: Domain toggle ---
const bDomain = document.getElementById('b-domain');
const rowSubteamBrand = document.getElementById('row-subteam-brand');
const rowEvent = document.getElementById('row-event');
bDomain.addEventListener('change', () => {
if (bDomain.value === 'EVNT') {
rowSubteamBrand.classList.add('hidden');
rowEvent.classList.remove('hidden');
} else {
rowSubteamBrand.classList.remove('hidden');
rowEvent.classList.add('hidden');
}
updateBuildPreview();
});
// --- Build: Live preview ---
const buildFields = ['b-omgid', 'b-domain', 'b-subteam', 'b-brand', 'b-event', 'b-initiative', 'b-yy', 'b-seq', 'b-assetname', 'b-version'];
buildFields.forEach(id => {
const el = document.getElementById(id);
el.addEventListener('input', updateBuildPreview);
el.addEventListener('change', updateBuildPreview);
});
function getBuildValues() {
return {
OMGID: document.getElementById('b-omgid').value.trim(),
Domain: document.getElementById('b-domain').value,
Subteam: document.getElementById('b-subteam').value,
Brand: document.getElementById('b-brand').value,
Event: document.getElementById('b-event').value,
Initiative: document.getElementById('b-initiative').value.trim().toUpperCase(),
YY: document.getElementById('b-yy').value.trim(),
Sequence: document.getElementById('b-seq').value.trim(),
AssetName: document.getElementById('b-assetname').value.trim().replace(/\s+/g, ''),
Version: document.getElementById('b-version').value.trim()
};
}
function buildFilename(v) {
let middle = '';
if (v.Domain === 'EVNT') {
const parts = ['EVNT'];
if (v.Event) parts.push(v.Event);
if (v.YY) parts.push(v.YY);
if (v.Sequence) parts.push(v.Sequence);
middle = parts.join('-');
} else {
const parts = [];
if (v.Domain) parts.push(v.Domain);
if (v.Subteam) parts.push(v.Subteam);
if (v.Brand) parts.push(v.Brand);
if (v.Initiative) parts.push(v.Initiative);
if (v.YY) parts.push(v.YY);
if (v.Sequence) parts.push(v.Sequence);
middle = parts.join('-');
}
let suffix = '';
if (v.AssetName) suffix += '_' + v.AssetName;
if (v.Version) suffix += '_v' + v.Version;
if (!v.OMGID && !middle) return '--';
return (v.OMGID ? v.OMGID + ' - ' : '') + middle + suffix;
}
function updateBuildPreview() {
const v = getBuildValues();
document.getElementById('build-preview').textContent = buildFilename(v);
}
// Set default year
document.getElementById('b-yy').value = new Date().getFullYear().toString().slice(-2);
updateBuildPreview();
// --- Build: Clear ---
document.getElementById('clearBuildBtn').addEventListener('click', () => {
buildFields.forEach(id => {
const el = document.getElementById(id);
if (el.tagName === 'SELECT') el.selectedIndex = 0;
else if (id === 'b-yy') el.value = new Date().getFullYear().toString().slice(-2);
else el.value = '';
});
rowSubteamBrand.classList.remove('hidden');
rowEvent.classList.add('hidden');
updateBuildPreview();
});
// --- Build: Push to Naming Tool ---
document.getElementById('pushBuildBtn').addEventListener('click', () => {
const v = getBuildValues();
pushToTool(v);
});
// --- Decode ---
document.getElementById('decodeBtn').addEventListener('click', decodeJobName);
document.getElementById('decode-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') decodeJobName();
});
let decodedValues = null;
function decodeJobName() {
const raw = document.getElementById('decode-input').value.trim();
const errorEl = document.getElementById('decode-error');
const resultEl = document.getElementById('decode-result');
errorEl.style.display = 'none';
resultEl.style.display = 'none';
if (!raw) {
errorEl.textContent = 'Please paste a job name to decode.';
errorEl.style.display = 'block';
return;
}
try {
decodedValues = parseJobName(raw);
renderBreakdown(decodedValues);
document.getElementById('decode-preview').textContent = buildFilename(decodedValues);
resultEl.style.display = 'block';
} catch (err) {
errorEl.textContent = 'Could not decode: ' + err.message;
errorEl.style.display = 'block';
}
}
function parseJobName(str) {
const result = { OMGID: '', Domain: '', Subteam: '', Brand: '', Event: '', Initiative: '', YY: '', Sequence: '', AssetName: '', Version: '' };
// Split on " - " to get OMGID and rest
let rest = str;
if (str.includes(' - ')) {
const idx = str.indexOf(' - ');
result.OMGID = str.substring(0, idx).trim();
rest = str.substring(idx + 3).trim();
}
// Split rest into core (before first _) and suffix (after first _)
let corePart = rest;
let suffixPart = '';
const underscoreIdx = rest.indexOf('_');
if (underscoreIdx !== -1) {
corePart = rest.substring(0, underscoreIdx);
suffixPart = rest.substring(underscoreIdx + 1);
}
// Parse suffix: AssetName_vN
if (suffixPart) {
const vMatch = suffixPart.match(/^(.+?)_v(\d+)$/);
if (vMatch) {
result.AssetName = vMatch[1];
result.Version = vMatch[2];
} else if (suffixPart.match(/^v(\d+)$/)) {
result.Version = suffixPart.replace('v', '');
} else {
result.AssetName = suffixPart;
}
}
// Parse core: Domain-Subteam-Brand-Initiative-YY-Seq (or EVNT-Event-YY-Seq)
const parts = corePart.split('-');
if (parts.length === 0) throw new Error('No segments found');
const domain = parts[0];
if (!DOMAIN_NAMES[domain]) throw new Error('Unknown domain: ' + domain);
result.Domain = domain;
if (domain === 'EVNT') {
// EVNT-[Event]-[YY]-[Seq]
if (parts.length >= 2) {
// Check if part is a known event
if (ALL_EVENTS.includes(parts[1])) {
result.Event = parts[1];
if (parts.length >= 3) result.YY = parts[2];
if (parts.length >= 4) result.Sequence = parts[3];
} else {
// Maybe YY directly
result.YY = parts[1];
if (parts.length >= 3) result.Sequence = parts[2];
}
}
} else {
// [Domain]-[Subteam]-[Brand]-[Initiative]-[YY]-[Seq]
let i = 1;
if (i < parts.length && ALL_SUBTEAMS.includes(parts[i])) {
result.Subteam = parts[i]; i++;
}
if (i < parts.length && ALL_BRANDS.includes(parts[i])) {
result.Brand = parts[i]; i++;
}
// Remaining: could be Initiative, YY, Seq
// YY is 2 digits, Seq is 2 digits
// Initiative is anything that's not a 2-digit number at the end
const remaining = parts.slice(i);
if (remaining.length >= 3) {
// Initiative-YY-Seq
result.Initiative = remaining.slice(0, -2).join('-');
result.YY = remaining[remaining.length - 2];
result.Sequence = remaining[remaining.length - 1];
} else if (remaining.length === 2) {
// Could be YY-Seq or Initiative-YY
if (remaining[0].match(/^\d{2}$/) && remaining[1].match(/^\d{2}$/)) {
result.YY = remaining[0];
result.Sequence = remaining[1];
} else {
result.Initiative = remaining[0];
result.YY = remaining[1];
}
} else if (remaining.length === 1) {
if (remaining[0].match(/^\d{2}$/)) {
result.YY = remaining[0];
} else {
result.Initiative = remaining[0];
}
}
}
return result;
}
function renderBreakdown(v) {
const container = document.getElementById('decode-breakdown');
container.innerHTML = '';
const fields = [
{ key: 'OMGID', label: 'OMGID' },
{ key: 'Domain', label: 'Domain', nameMap: DOMAIN_NAMES },
{ key: 'Subteam', label: 'Subteam', nameMap: SUBTEAM_NAMES },
{ key: 'Brand', label: 'Brand', nameMap: BRAND_NAMES },
{ key: 'Event', label: 'Event', nameMap: EVENT_NAMES },
{ key: 'Initiative', label: 'Initiative' },
{ key: 'YY', label: 'Year (YY)' },
{ key: 'Sequence', label: 'Sequence' },
{ key: 'AssetName', label: 'Asset Name' },
{ key: 'Version', label: 'Version' }
];
fields.forEach(f => {
const val = v[f.key];
if (!val) return; // skip empty
const item = document.createElement('div');
item.className = 'breakdown-item';
let fullName = '';
if (f.nameMap && f.nameMap[val]) {
fullName = f.nameMap[val];
}
item.innerHTML = `
<div class="label">${f.label}</div>
<div class="value">${val}</div>
${fullName ? '<div class="full-name">' + fullName + '</div>' : ''}
`;
container.appendChild(item);
});
}
// --- Decode: Push ---
document.getElementById('pushDecodeBtn').addEventListener('click', () => {
if (decodedValues) pushToTool(decodedValues);
});
// --- Decode: Edit in Builder ---
document.getElementById('editDecodeBtn').addEventListener('click', () => {
if (!decodedValues) return;
populateBuildForm(decodedValues);
// Switch to build tab
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.querySelector('[data-tab="build"]').classList.add('active');
document.getElementById('tab-build').classList.add('active');
});
function populateBuildForm(v) {
document.getElementById('b-omgid').value = v.OMGID || '';
document.getElementById('b-domain').value = v.Domain || '';
document.getElementById('b-subteam').value = v.Subteam || '';
document.getElementById('b-brand').value = v.Brand || '';
document.getElementById('b-event').value = v.Event || '';
document.getElementById('b-initiative').value = v.Initiative || '';
document.getElementById('b-yy').value = v.YY || '';
document.getElementById('b-seq').value = v.Sequence || '';
document.getElementById('b-assetname').value = v.AssetName || '';
document.getElementById('b-version').value = v.Version || '';
if (v.Domain === 'EVNT') {
rowSubteamBrand.classList.add('hidden');
rowEvent.classList.remove('hidden');
} else {
rowSubteamBrand.classList.remove('hidden');
rowEvent.classList.add('hidden');
}
updateBuildPreview();
}
// --- Push to Naming Tool ---
function pushToTool(v) {
const params = new URLSearchParams();
params.set('builder', '1');
Object.keys(v).forEach(key => {
if (v[key]) params.set(key, v[key]);
});
window.location.href = 'index.php?' + params.toString();
}
})();
</script>
</body>
</html>