import cursor files
This commit is contained in:
185
assessments/index.html
Normal file
185
assessments/index.html
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Assessments · Agile Maturity</title>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0b0d10;--card:#12161b;--text:#e6edf3;--muted:#9da7b3;--brand:#5bb8ff}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;background:var(--bg);color:var(--text)}
|
||||||
|
.container{max-width:960px;margin:0 auto;padding:20px}
|
||||||
|
a{color:var(--brand)}
|
||||||
|
.card{background:var(--card);border:1px solid #222831;border-radius:12px;padding:20px}
|
||||||
|
input[type="text"],input[type="number"],select,textarea{background:#0d1117;border:1px solid #1f2630;border-radius:8px;padding:10px;color:var(--text);width:100%}
|
||||||
|
button{background:var(--brand);color:#00111f;border:0;border-radius:8px;padding:10px 14px;cursor:pointer;font-weight:600}
|
||||||
|
button.secondary{background:#232b36;color:var(--text);border:1px solid #2c3644}
|
||||||
|
.row{display:flex;gap:12px;flex-wrap:wrap}
|
||||||
|
pre{background:#0d1117;border:1px solid #1f2630;border-radius:10px;padding:12px;overflow:auto}
|
||||||
|
label{display:block;font-size:12px;color:var(--muted);margin-top:8px}
|
||||||
|
.dimension{display:grid;grid-template-columns:1fr 120px;gap:8px;margin:6px 0}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p><a href="../team/index.html">← Teams</a> · <a href="../index.html">Home</a></p>
|
||||||
|
<h1>Assessments</h1>
|
||||||
|
<section class="card">
|
||||||
|
<h3>Create an assessment</h3>
|
||||||
|
<div class="row">
|
||||||
|
<select id="companySelect"></select>
|
||||||
|
<select id="teamSelect"></select>
|
||||||
|
<input id="assessor" type="text" placeholder="Assessor name">
|
||||||
|
<input id="framework" type="text" placeholder="Framework version (e.g., v1.0)">
|
||||||
|
</div>
|
||||||
|
<label>Dimensions (key and score 0..5)</label>
|
||||||
|
<div id="dims"></div>
|
||||||
|
<div class="row">
|
||||||
|
<button class="secondary" id="addDim">Add dimension</button>
|
||||||
|
<button id="saveAssessment">Save assessment</button>
|
||||||
|
</div>
|
||||||
|
<label>Assessments for selected team</label>
|
||||||
|
<div id="list"></div>
|
||||||
|
<div class="row" style="margin-top:12px">
|
||||||
|
<button class="secondary" id="exportBtn">Export</button>
|
||||||
|
<input id="importFile" type="file" accept="application/json">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const assessKey = 'assessments';
|
||||||
|
function loadCompanies(){ try{ return JSON.parse(localStorage.getItem('companies')||'[]'); }catch{ return []; } }
|
||||||
|
function loadTeams(){ try{ return JSON.parse(localStorage.getItem('teams')||'[]'); }catch{ return []; } }
|
||||||
|
function loadAssessments(){ try{ return JSON.parse(localStorage.getItem(assessKey)||'[]'); }catch{ return []; } }
|
||||||
|
function saveAssessments(list){ localStorage.setItem(assessKey, JSON.stringify(list)); }
|
||||||
|
function uuid(){ return (crypto && crypto.randomUUID) ? crypto.randomUUID() : 'id-'+Math.random().toString(36).slice(2); }
|
||||||
|
|
||||||
|
function initSelectors(){
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const cid = params.get('companyId');
|
||||||
|
const tid = params.get('teamId');
|
||||||
|
const companies = loadCompanies();
|
||||||
|
const cs = document.getElementById('companySelect');
|
||||||
|
cs.innerHTML = '';
|
||||||
|
companies.forEach(c=>{ const o=document.createElement('option'); o.value=c.id; o.textContent=c.name; cs.appendChild(o); });
|
||||||
|
if(cid) cs.value = cid;
|
||||||
|
cs.addEventListener('change', ()=>{ fillTeams(); render(); });
|
||||||
|
fillTeams();
|
||||||
|
const ts = document.getElementById('teamSelect');
|
||||||
|
if(tid) ts.value = tid;
|
||||||
|
ts.addEventListener('change', render);
|
||||||
|
}
|
||||||
|
function fillTeams(){
|
||||||
|
const ts = document.getElementById('teamSelect');
|
||||||
|
const cid = document.getElementById('companySelect').value;
|
||||||
|
const teams = loadTeams().filter(t=>t.companyId===cid);
|
||||||
|
ts.innerHTML = '';
|
||||||
|
teams.forEach(t=>{ const o=document.createElement('option'); o.value=t.id; o.textContent=t.name; ts.appendChild(o); });
|
||||||
|
}
|
||||||
|
function addDimView(key='', score=''){
|
||||||
|
const dims = document.getElementById('dims');
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.className = 'dimension';
|
||||||
|
wrap.innerHTML = `<input type="text" placeholder="key (e.g., teamwork)" value="${key}"><input type="number" min="0" max="5" step="1" placeholder="score" value="${score}">`;
|
||||||
|
dims.appendChild(wrap);
|
||||||
|
}
|
||||||
|
function gatherDims(){
|
||||||
|
return Array.from(document.querySelectorAll('#dims .dimension')).map(d=>{
|
||||||
|
const [kEl,sEl] = d.querySelectorAll('input');
|
||||||
|
const k = (kEl.value||'').trim();
|
||||||
|
const s = Number(sEl.value||0);
|
||||||
|
if(!k) return null;
|
||||||
|
return { key:k, score: isNaN(s)?0:Math.max(0,Math.min(5,s)) };
|
||||||
|
}).filter(Boolean);
|
||||||
|
}
|
||||||
|
function onSave(){
|
||||||
|
const companyId = document.getElementById('companySelect').value;
|
||||||
|
const teamId = document.getElementById('teamSelect').value;
|
||||||
|
if(!companyId || !teamId){ alert('Choose company and team'); return; }
|
||||||
|
const assessor = document.getElementById('assessor').value.trim();
|
||||||
|
const frameworkVersion = document.getElementById('framework').value.trim() || 'v1.0';
|
||||||
|
const dimensions = gatherDims();
|
||||||
|
if(dimensions.length===0){ alert('Add at least one dimension'); return; }
|
||||||
|
const overall = dimensions.reduce((a,d)=>a+d.score,0)/dimensions.length;
|
||||||
|
const list = loadAssessments();
|
||||||
|
list.push({ id: uuid(), companyId, teamId, assessedAt: new Date().toISOString(), assessor: { name: assessor }, frameworkVersion, dimensions, overallScore: Number(overall.toFixed(2)), version: 1 });
|
||||||
|
saveAssessments(list);
|
||||||
|
document.getElementById('dims').innerHTML='';
|
||||||
|
addDimView();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function render(){
|
||||||
|
const cid = document.getElementById('companySelect').value;
|
||||||
|
const tid = document.getElementById('teamSelect').value;
|
||||||
|
const list = document.getElementById('list');
|
||||||
|
const data = loadAssessments().filter(a=>a.companyId===cid && a.teamId===tid).sort((a,b)=>b.assessedAt.localeCompare(a.assessedAt));
|
||||||
|
if(data.length===0){ list.innerHTML = '<p class="small">No assessments yet.</p>'; return; }
|
||||||
|
list.innerHTML = '';
|
||||||
|
data.forEach(a=>{
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'row';
|
||||||
|
row.style.alignItems='center';
|
||||||
|
row.style.justifyContent='space-between';
|
||||||
|
const info = document.createElement('div');
|
||||||
|
info.textContent = `${new Date(a.assessedAt).toLocaleString()} · Overall ${a.overallScore}`;
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'row';
|
||||||
|
const btnView = document.createElement('button');
|
||||||
|
btnView.className = 'secondary';
|
||||||
|
btnView.textContent = 'View JSON';
|
||||||
|
btnView.onclick = ()=>{
|
||||||
|
const blob = new Blob([JSON.stringify(a,null,2)], {type:'application/json'});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
window.open(url, '_blank');
|
||||||
|
setTimeout(()=>URL.revokeObjectURL(url), 5000);
|
||||||
|
};
|
||||||
|
const btnDel = document.createElement('button');
|
||||||
|
btnDel.textContent = 'Delete';
|
||||||
|
btnDel.style.background = '#ff6666';
|
||||||
|
btnDel.onclick = ()=>{
|
||||||
|
const rest = loadAssessments().filter(x=>x.id!==a.id);
|
||||||
|
saveAssessments(rest);
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
actions.append(btnView, btnDel);
|
||||||
|
row.append(info, actions);
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function onExport(){
|
||||||
|
const cid = document.getElementById('companySelect').value;
|
||||||
|
const tid = document.getElementById('teamSelect').value;
|
||||||
|
const data = loadAssessments().filter(a=>a.companyId===cid && a.teamId===tid);
|
||||||
|
const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'});
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = `assessments.${cid}.${tid}.export.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
}
|
||||||
|
function onImport(ev){
|
||||||
|
const file = ev.target.files && ev.target.files[0];
|
||||||
|
if(!file) return;
|
||||||
|
file.text().then(text=>{
|
||||||
|
try{
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
if(!Array.isArray(json)) throw new Error('Expected an array');
|
||||||
|
const existing = loadAssessments();
|
||||||
|
saveAssessments([...existing, ...json]);
|
||||||
|
render();
|
||||||
|
}catch(e){ alert('Invalid JSON: '+e.message); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById('addDim').addEventListener('click', ()=>addDimView());
|
||||||
|
document.getElementById('saveAssessment').addEventListener('click', onSave);
|
||||||
|
document.getElementById('exportBtn').addEventListener('click', onExport);
|
||||||
|
document.getElementById('importFile').addEventListener('change', onImport);
|
||||||
|
initSelectors();
|
||||||
|
addDimView();
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
152
company/index.html
Normal file
152
company/index.html
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Companies · Agile Maturity</title>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0b0d10;--card:#12161b;--text:#e6edf3;--muted:#9da7b3;--brand:#5bb8ff}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;background:var(--bg);color:var(--text)}
|
||||||
|
.container{max-width:960px;margin:0 auto;padding:20px}
|
||||||
|
a{color:var(--brand)}
|
||||||
|
.card{background:var(--card);border:1px solid #222831;border-radius:12px;padding:20px}
|
||||||
|
input[type="text"],input[type="file"]{background:#0d1117;border:1px solid #1f2630;border-radius:8px;padding:10px;color:var(--text);width:100%}
|
||||||
|
button{background:var(--brand);color:#00111f;border:0;border-radius:8px;padding:10px 14px;cursor:pointer;font-weight:600}
|
||||||
|
button.secondary{background:#232b36;color:var(--text);border:1px solid #2c3644}
|
||||||
|
.row{display:flex;gap:12px;flex-wrap:wrap}
|
||||||
|
pre{background:#0d1117;border:1px solid #1f2630;border-radius:10px;padding:12px;overflow:auto}
|
||||||
|
label{display:block;font-size:12px;color:var(--muted);margin-top:8px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p><a href="../index.html">← Back</a></p>
|
||||||
|
<h1>Companies</h1>
|
||||||
|
<section class="card">
|
||||||
|
<h3>Create or edit a company</h3>
|
||||||
|
<div class="row">
|
||||||
|
<input id="cName" type="text" placeholder="Name">
|
||||||
|
<input id="cDomain" type="text" placeholder="Domain (optional)">
|
||||||
|
<button id="saveCompany">Save</button>
|
||||||
|
<button class="secondary" id="resetForm">Reset</button>
|
||||||
|
</div>
|
||||||
|
<label>Companies</label>
|
||||||
|
<div id="list"></div>
|
||||||
|
<div class="row" style="margin-top:12px">
|
||||||
|
<button class="secondary" id="exportBtn">Export</button>
|
||||||
|
<input id="importFile" type="file" accept="application/json">
|
||||||
|
</div>
|
||||||
|
<pre id="debug" style="display:none"></pre>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const storageKey = 'companies';
|
||||||
|
let editId = null;
|
||||||
|
|
||||||
|
function loadCompanies(){
|
||||||
|
try{ return JSON.parse(localStorage.getItem(storageKey)||'[]'); }catch{ return []; }
|
||||||
|
}
|
||||||
|
function saveCompanies(list){
|
||||||
|
localStorage.setItem(storageKey, JSON.stringify(list));
|
||||||
|
}
|
||||||
|
function uuid(){ return (crypto && crypto.randomUUID) ? crypto.randomUUID() : 'id-'+Math.random().toString(36).slice(2); }
|
||||||
|
function render(){
|
||||||
|
const companies = loadCompanies();
|
||||||
|
const list = document.getElementById('list');
|
||||||
|
if(companies.length===0){ list.innerHTML = '<p class="small">No companies yet.</p>'; return; }
|
||||||
|
list.innerHTML = '';
|
||||||
|
companies.forEach(c=>{
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'row';
|
||||||
|
row.style.alignItems = 'center';
|
||||||
|
row.style.justifyContent = 'space-between';
|
||||||
|
const info = document.createElement('div');
|
||||||
|
info.textContent = `${c.name}${c.domain? ' · '+c.domain: ''}`;
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'row';
|
||||||
|
const btnTeams = document.createElement('button');
|
||||||
|
btnTeams.className = 'secondary';
|
||||||
|
btnTeams.textContent = 'Teams';
|
||||||
|
btnTeams.onclick = ()=>{ window.location.href = `../team/index.html?companyId=${encodeURIComponent(c.id)}` };
|
||||||
|
const btnEdit = document.createElement('button');
|
||||||
|
btnEdit.className = 'secondary';
|
||||||
|
btnEdit.textContent = 'Edit';
|
||||||
|
btnEdit.onclick = ()=>{
|
||||||
|
editId = c.id;
|
||||||
|
document.getElementById('cName').value = c.name || '';
|
||||||
|
document.getElementById('cDomain').value = c.domain || '';
|
||||||
|
};
|
||||||
|
const btnDel = document.createElement('button');
|
||||||
|
btnDel.textContent = 'Delete';
|
||||||
|
btnDel.style.background = '#ff6666';
|
||||||
|
btnDel.onclick = ()=>{
|
||||||
|
if(!confirm('Delete company and its teams/assessments references?')) return;
|
||||||
|
const rest = loadCompanies().filter(x=>x.id!==c.id);
|
||||||
|
saveCompanies(rest);
|
||||||
|
// cascade delete teams/assessments stored locally
|
||||||
|
const teams = JSON.parse(localStorage.getItem('teams')||'[]').filter(t=>t.companyId!==c.id);
|
||||||
|
localStorage.setItem('teams', JSON.stringify(teams));
|
||||||
|
const assessments = JSON.parse(localStorage.getItem('assessments')||'[]').filter(a=>a.companyId!==c.id);
|
||||||
|
localStorage.setItem('assessments', JSON.stringify(assessments));
|
||||||
|
if(editId===c.id) resetForm();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
actions.append(btnTeams, btnEdit, btnDel);
|
||||||
|
row.append(info, actions);
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function resetForm(){
|
||||||
|
editId = null;
|
||||||
|
document.getElementById('cName').value = '';
|
||||||
|
document.getElementById('cDomain').value = '';
|
||||||
|
}
|
||||||
|
function onSave(){
|
||||||
|
const name = document.getElementById('cName').value.trim();
|
||||||
|
const domain = document.getElementById('cDomain').value.trim();
|
||||||
|
if(!name){ alert('Name is required'); return; }
|
||||||
|
const companies = loadCompanies();
|
||||||
|
if(editId){
|
||||||
|
const idx = companies.findIndex(c=>c.id===editId);
|
||||||
|
if(idx>=0){ companies[idx] = { ...companies[idx], name, domain }; }
|
||||||
|
}else{
|
||||||
|
companies.push({ id: uuid(), name, domain, createdAt: new Date().toISOString(), version: 1 });
|
||||||
|
}
|
||||||
|
saveCompanies(companies);
|
||||||
|
resetForm();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function onExport(){
|
||||||
|
const data = loadCompanies();
|
||||||
|
const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'});
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = 'companies.export.json';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
}
|
||||||
|
function onImport(ev){
|
||||||
|
const file = ev.target.files && ev.target.files[0];
|
||||||
|
if(!file) return;
|
||||||
|
file.text().then(text=>{
|
||||||
|
try{
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
if(!Array.isArray(json)) throw new Error('Expected an array');
|
||||||
|
saveCompanies(json);
|
||||||
|
render();
|
||||||
|
}catch(e){ alert('Invalid JSON: '+e.message); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById('saveCompany').addEventListener('click', onSave);
|
||||||
|
document.getElementById('resetForm').addEventListener('click', resetForm);
|
||||||
|
document.getElementById('exportBtn').addEventListener('click', onExport);
|
||||||
|
document.getElementById('importFile').addEventListener('change', onImport);
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
13
config.json
Normal file
13
config.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"appName": "Agile Maturity",
|
||||||
|
"version": 1,
|
||||||
|
"dataRoot": "data",
|
||||||
|
"schemas": {
|
||||||
|
"company": "schemas/company.schema.json",
|
||||||
|
"team": "schemas/team.schema.json",
|
||||||
|
"assessment": "schemas/assessment.schema.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
110
index.html
Normal file
110
index.html
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Agile Maturity</title>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0b0d10;--card:#12161b;--text:#e6edf3;--muted:#9da7b3;--brand:#5bb8ff;--accent:#7ee787;--danger:#ff6666}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;background:var(--bg);color:var(--text)}
|
||||||
|
header{padding:20px}
|
||||||
|
.container{max-width:960px;margin:0 auto;padding:20px}
|
||||||
|
.card{background:var(--card);border:1px solid #222831;border-radius:12px;padding:20px}
|
||||||
|
h1{margin:0 0 8px;font-size:24px}
|
||||||
|
p{margin:0 0 16px;color:var(--muted)}
|
||||||
|
.grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}
|
||||||
|
a.tile{display:block;padding:16px;border-radius:10px;background:#141a21;border:1px solid #1f2630;color:var(--text);text-decoration:none}
|
||||||
|
a.tile:hover{border-color:#2a3442;background:#161d25}
|
||||||
|
.small{font-size:12px;color:var(--muted)}
|
||||||
|
code.inline{background:#0d1117;border:1px solid #1f2630;border-radius:6px;padding:2px 6px}
|
||||||
|
footer{opacity:.7;margin-top:24px;font-size:12px}
|
||||||
|
.row{display:flex;gap:12px;flex-wrap:wrap}
|
||||||
|
input[type="text"]{background:#0d1117;border:1px solid #1f2630;border-radius:8px;padding:10px;color:var(--text);width:100%;}
|
||||||
|
button{background:var(--brand);color:#00111f;border:0;border-radius:8px;padding:10px 14px;cursor:pointer;font-weight:600}
|
||||||
|
button.secondary{background:#232b36;color:var(--text);border:1px solid #2c3644}
|
||||||
|
pre{background:#0d1117;border:1px solid #1f2630;border-radius:10px;padding:12px;overflow:auto}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header class="container">
|
||||||
|
<h1>Agile Maturity</h1>
|
||||||
|
<p>Manage companies, teams and assessments stored as JSON files.</p>
|
||||||
|
</header>
|
||||||
|
<main class="container">
|
||||||
|
<section class="card" id="app">
|
||||||
|
<div class="grid">
|
||||||
|
<a class="tile" href="company/index.html">
|
||||||
|
<h3>Companies</h3>
|
||||||
|
<p class="small">Create or load a company file</p>
|
||||||
|
</a>
|
||||||
|
<a class="tile" href="team/index.html">
|
||||||
|
<h3>Teams</h3>
|
||||||
|
<p class="small">Create or load a team file</p>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<hr style="border:none;border-top:1px solid #222831;margin:20px 0">
|
||||||
|
<div>
|
||||||
|
<h3>Configuration</h3>
|
||||||
|
<p class="small">Reads <code class="inline">config.json</code> at project root.</p>
|
||||||
|
<div class="row">
|
||||||
|
<button class="secondary" id="reloadCfg">Reload config</button>
|
||||||
|
<button id="openCfg">Open config.json</button>
|
||||||
|
</div>
|
||||||
|
<pre id="cfgOut">Loading config...</pre>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<footer>Single-file static app — no backend required.</footer>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
function migrateLegacy(){
|
||||||
|
try{
|
||||||
|
const migrated = localStorage.getItem('migratedFromLegacy');
|
||||||
|
const hasCompanies = JSON.parse(localStorage.getItem('companies')||'[]').length>0;
|
||||||
|
const legacy = JSON.parse(localStorage.getItem('agileMaturity')||'null');
|
||||||
|
if(migrated || hasCompanies || !legacy){ return; }
|
||||||
|
const companyId = (crypto&&crypto.randomUUID)?crypto.randomUUID():'id-'+Math.random().toString(36).slice(2);
|
||||||
|
const company = {
|
||||||
|
id: companyId,
|
||||||
|
name: legacy.org?.name || 'Organisation',
|
||||||
|
domain: legacy.org?.sector || '',
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
version: 1
|
||||||
|
};
|
||||||
|
const teams = (legacy.teams||[]).map(t=>({
|
||||||
|
id: t.id || ((crypto&&crypto.randomUUID)?crypto.randomUUID():'id-'+Math.random().toString(36).slice(2)),
|
||||||
|
companyId,
|
||||||
|
name: t.name || 'Equipe',
|
||||||
|
coach: t.lead || '',
|
||||||
|
createdAt: t.createdAt || new Date().toISOString(),
|
||||||
|
version: 1
|
||||||
|
}));
|
||||||
|
localStorage.setItem('companies', JSON.stringify([company]));
|
||||||
|
localStorage.setItem('teams', JSON.stringify(teams));
|
||||||
|
localStorage.setItem('migratedFromLegacy', 'true');
|
||||||
|
}catch(e){ console.warn('Legacy migration skipped:', e); }
|
||||||
|
}
|
||||||
|
async function loadConfig(){
|
||||||
|
const out = document.getElementById('cfgOut');
|
||||||
|
try{
|
||||||
|
const res = await fetch('config.json', {cache:'no-store'});
|
||||||
|
if(!res.ok) throw new Error('HTTP '+res.status);
|
||||||
|
const cfg = await res.json();
|
||||||
|
out.textContent = JSON.stringify(cfg, null, 2);
|
||||||
|
}catch(err){
|
||||||
|
out.textContent = 'Could not load config.json. '+err.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.getElementById('reloadCfg').addEventListener('click', loadConfig);
|
||||||
|
document.getElementById('openCfg').addEventListener('click', ()=>{
|
||||||
|
window.location.href = 'config.json';
|
||||||
|
});
|
||||||
|
migrateLegacy();
|
||||||
|
loadConfig();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
171
team/index.html
Normal file
171
team/index.html
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Teams · Agile Maturity</title>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
|
<style>
|
||||||
|
:root{--bg:#0b0d10;--card:#12161b;--text:#e6edf3;--muted:#9da7b3;--brand:#5bb8ff}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
body{margin:0;font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;background:var(--bg);color:var(--text)}
|
||||||
|
.container{max-width:960px;margin:0 auto;padding:20px}
|
||||||
|
a{color:var(--brand)}
|
||||||
|
.card{background:var(--card);border:1px solid #222831;border-radius:12px;padding:20px}
|
||||||
|
input[type="text"],input[type="file"]{background:#0d1117;border:1px solid #1f2630;border-radius:8px;padding:10px;color:var(--text);width:100%}
|
||||||
|
button{background:var(--brand);color:#00111f;border:0;border-radius:8px;padding:10px 14px;cursor:pointer;font-weight:600}
|
||||||
|
button.secondary{background:#232b36;color:var(--text);border:1px solid #2c3644}
|
||||||
|
.row{display:flex;gap:12px;flex-wrap:wrap}
|
||||||
|
pre{background:#0d1117;border:1px solid #1f2630;border-radius:10px;padding:12px;overflow:auto}
|
||||||
|
label{display:block;font-size:12px;color:var(--muted);margin-top:8px}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<p><a href="../company/index.html">← Companies</a> · <a href="../index.html">Home</a></p>
|
||||||
|
<h1>Teams</h1>
|
||||||
|
<section class="card">
|
||||||
|
<h3>Create or edit a team</h3>
|
||||||
|
<div class="row">
|
||||||
|
<input id="tName" type="text" placeholder="Team name">
|
||||||
|
<input id="tCoach" type="text" placeholder="Coach (optional)">
|
||||||
|
<button id="saveTeam">Save</button>
|
||||||
|
<button class="secondary" id="resetForm">Reset</button>
|
||||||
|
</div>
|
||||||
|
<label>Company</label>
|
||||||
|
<div class="row">
|
||||||
|
<select id="companySelect"></select>
|
||||||
|
<button class="secondary" id="goCompany">Open company</button>
|
||||||
|
</div>
|
||||||
|
<label>Teams for selected company</label>
|
||||||
|
<div id="list"></div>
|
||||||
|
<div class="row" style="margin-top:12px">
|
||||||
|
<button class="secondary" id="exportBtn">Export</button>
|
||||||
|
<input id="importFile" type="file" accept="application/json">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const teamKey = 'teams';
|
||||||
|
let editId = null;
|
||||||
|
|
||||||
|
function loadCompanies(){ try{ return JSON.parse(localStorage.getItem('companies')||'[]'); }catch{ return []; } }
|
||||||
|
function loadTeams(){ try{ return JSON.parse(localStorage.getItem(teamKey)||'[]'); }catch{ return []; } }
|
||||||
|
function saveTeams(list){ localStorage.setItem(teamKey, JSON.stringify(list)); }
|
||||||
|
function uuid(){ return (crypto && crypto.randomUUID) ? crypto.randomUUID() : 'id-'+Math.random().toString(36).slice(2); }
|
||||||
|
|
||||||
|
function initCompanySelect(){
|
||||||
|
const companies = loadCompanies();
|
||||||
|
const sel = document.getElementById('companySelect');
|
||||||
|
sel.innerHTML = '';
|
||||||
|
companies.forEach(c=>{
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = c.id; opt.textContent = c.name + (c.domain? ' · '+c.domain:'');
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const cid = params.get('companyId');
|
||||||
|
if(cid){ sel.value = cid; }
|
||||||
|
sel.addEventListener('change', render);
|
||||||
|
document.getElementById('goCompany').onclick = ()=>{
|
||||||
|
const id = sel.value; if(id) location.href = `../company/index.html#${encodeURIComponent(id)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function render(){
|
||||||
|
const cid = document.getElementById('companySelect').value;
|
||||||
|
const list = document.getElementById('list');
|
||||||
|
const teams = loadTeams().filter(t=>t.companyId===cid);
|
||||||
|
if(teams.length===0){ list.innerHTML = '<p class="small">No teams for this company.</p>'; return; }
|
||||||
|
list.innerHTML = '';
|
||||||
|
teams.forEach(t=>{
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'row';
|
||||||
|
row.style.alignItems = 'center';
|
||||||
|
row.style.justifyContent = 'space-between';
|
||||||
|
const info = document.createElement('div');
|
||||||
|
info.textContent = `${t.name}${t.coach? ' · Coach: '+t.coach:''}`;
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'row';
|
||||||
|
const btnAssess = document.createElement('button');
|
||||||
|
btnAssess.className = 'secondary';
|
||||||
|
btnAssess.textContent = 'Assessments';
|
||||||
|
btnAssess.onclick = ()=>{ location.href = `../assessments/index.html?companyId=${encodeURIComponent(t.companyId)}&teamId=${encodeURIComponent(t.id)}` };
|
||||||
|
const btnEdit = document.createElement('button');
|
||||||
|
btnEdit.className = 'secondary';
|
||||||
|
btnEdit.textContent = 'Edit';
|
||||||
|
btnEdit.onclick = ()=>{
|
||||||
|
editId = t.id;
|
||||||
|
document.getElementById('tName').value = t.name || '';
|
||||||
|
document.getElementById('tCoach').value = t.coach || '';
|
||||||
|
};
|
||||||
|
const btnDel = document.createElement('button');
|
||||||
|
btnDel.textContent = 'Delete';
|
||||||
|
btnDel.style.background = '#ff6666';
|
||||||
|
btnDel.onclick = ()=>{
|
||||||
|
if(!confirm('Delete team and its assessments?')) return;
|
||||||
|
const rest = loadTeams().filter(x=>x.id!==t.id);
|
||||||
|
saveTeams(rest);
|
||||||
|
const assessments = JSON.parse(localStorage.getItem('assessments')||'[]').filter(a=>a.teamId!==t.id);
|
||||||
|
localStorage.setItem('assessments', JSON.stringify(assessments));
|
||||||
|
if(editId===t.id) resetForm();
|
||||||
|
render();
|
||||||
|
};
|
||||||
|
actions.append(btnAssess, btnEdit, btnDel);
|
||||||
|
row.append(info, actions);
|
||||||
|
list.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function resetForm(){ editId=null; document.getElementById('tName').value=''; document.getElementById('tCoach').value=''; }
|
||||||
|
function onSave(){
|
||||||
|
const name = document.getElementById('tName').value.trim();
|
||||||
|
const coach = document.getElementById('tCoach').value.trim();
|
||||||
|
const companyId = document.getElementById('companySelect').value;
|
||||||
|
if(!companyId){ alert('Select a company'); return; }
|
||||||
|
if(!name){ alert('Team name is required'); return; }
|
||||||
|
const teams = loadTeams();
|
||||||
|
if(editId){
|
||||||
|
const idx = teams.findIndex(x=>x.id===editId);
|
||||||
|
if(idx>=0){ teams[idx] = { ...teams[idx], name, coach }; }
|
||||||
|
}else{
|
||||||
|
teams.push({ id: uuid(), companyId, name, coach, createdAt: new Date().toISOString(), version: 1 });
|
||||||
|
}
|
||||||
|
saveTeams(teams);
|
||||||
|
resetForm();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
function onExport(){
|
||||||
|
const cid = document.getElementById('companySelect').value;
|
||||||
|
const data = loadTeams().filter(t=>t.companyId===cid);
|
||||||
|
const blob = new Blob([JSON.stringify(data,null,2)], {type:'application/json'});
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.download = `teams.${cid||'all'}.export.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(a.href);
|
||||||
|
}
|
||||||
|
function onImport(ev){
|
||||||
|
const file = ev.target.files && ev.target.files[0];
|
||||||
|
if(!file) return;
|
||||||
|
file.text().then(text=>{
|
||||||
|
try{
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
if(!Array.isArray(json)) throw new Error('Expected an array');
|
||||||
|
const existing = loadTeams();
|
||||||
|
const merged = [...existing, ...json];
|
||||||
|
saveTeams(merged);
|
||||||
|
render();
|
||||||
|
}catch(e){ alert('Invalid JSON: '+e.message); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.getElementById('saveTeam').addEventListener('click', onSave);
|
||||||
|
document.getElementById('resetForm').addEventListener('click', resetForm);
|
||||||
|
document.getElementById('exportBtn').addEventListener('click', onExport);
|
||||||
|
document.getElementById('importFile').addEventListener('change', onImport);
|
||||||
|
initCompanySelect();
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user