Skip to content

Commit 7f6a684

Browse files
John Heldrethclaude
andcommitted
feat: community voting & vendor self-assessment system
PHP (v2.0.0): - register_activation_hook + plugins_loaded to create two DB tables: wp_asrg_csms_votes (user agree/dispute per vendor×feature) wp_asrg_csms_vendor_claims (vendor self-assessments w/ pending/approved/rejected status) - Six REST endpoints under /wp-json/asrg-csms/v1/: GET /votes/summary – public vote tallies GET /votes/mine – authenticated user's own votes POST /votes – cast or retract a vote (same vote = toggle off) DELETE /votes/{id} – remove own vote GET /claims/approved – approved vendor responses (public) POST /claims – submit vendor self-assessment (company role + asrg_vendor_key required) - Shortcode now injects ASRG_AUTH, ASRG_REST, ASRG_VOTE_SUMMARY, ASRG_MY_VOTES, ASRG_VENDOR_CLAIMS, ASRG_MY_CLAIMS at render time - Vendor detection: WP company role + asrg_vendor_key user meta HTML/JS: - Vote tally badges (▲N ▼N) on every CSMS + IT score cell (org group excluded) - openModal() refactored to accept context object {vendorKey, featId, voteKey, inVotableGroup, …} - Modal hides vote section for org group features - Real vote counts loaded from ASRG_VOTE_SUMMARY; user's prior vote pre-selected - Submit/Sign-in buttons toggled based on auth state - doVote() redirects unauthenticated users to garage.asrg.io login - doSubmit() POSTs vote to REST API, updates in-memory summary, refreshes table tally cell - doLogin() redirects to garage.asrg.io with redirect_to= param - Approved vendor claims render as "Vendor Response" section in modal (visible to all) - Vendor users see self-assessment edit form with score picker + note textarea in their column - selectClaimScore() + submitClaim() POST claim to REST API with pending status - updateTableTally() refreshes ▲/▼ badge in table after vote without page reload Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5b26a99 commit 7f6a684

2 files changed

Lines changed: 621 additions & 34 deletions

File tree

plugin/asrg-csms-evaluation.html

Lines changed: 232 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,15 @@
181181
.blogin:hover{border-color:var(--text2);color:var(--text)}
182182
.anote{font-family:var(--mono);font-size:9px;color:var(--text3);margin-left:auto}
183183

184+
/* ── VOTE TALLIES (in score cells) ── */
185+
.vtally{display:flex;align-items:center;justify-content:center;gap:3px;margin-top:3px;font-family:var(--mono);font-size:7px;min-height:10px}
186+
.vt-up{color:var(--green);font-weight:600}
187+
.vt-dn{color:var(--red);font-weight:600}
188+
189+
/* ── VENDOR CLAIM SECTION (in modal) ── */
190+
.mclaim{border-top:1px solid var(--border);padding:14px 20px}
191+
.mclaim-approved{margin-bottom:12px}
192+
.mclaim-edit{border-top:1px solid var(--border);padding-top:14px;margin-top:4px}
184193

185194
::-webkit-scrollbar{width:5px;height:5px}
186195
::-webkit-scrollbar-track{background:transparent}
@@ -248,7 +257,7 @@ <h2>Automotive CSMS Tool Evaluation</h2>
248257
<div class="mlbl">Scoring Rationale</div>
249258
<div class="mrat" id="mRat"></div>
250259
</div>
251-
<div class="mfb">
260+
<div class="mfb" id="mfb">
252261
<div class="mflbl">Community Feedback <span class="achip">ASRG Members Only</span></div>
253262
<div class="vrow">
254263
<button class="vbtn up" id="vUp" onclick="doVote('up')">👍 Agree</button>
@@ -257,11 +266,12 @@ <h2>Automotive CSMS Tool Evaluation</h2>
257266
</div>
258267
<textarea class="ftxt" id="ftxt" placeholder="Add context, a correction, or a source URL… (ASRG portal sign-in required to submit)"></textarea>
259268
<div class="fact">
260-
<button class="bsub" onclick="doSubmit()">Submit Feedback</button>
261-
<button class="blogin" onclick="doLogin()">Sign in →</button>
269+
<button class="bsub" id="bsub" onclick="doSubmit()">Submit Feedback</button>
270+
<button class="blogin" id="blogin" onclick="doLogin()">Sign in →</button>
262271
<span class="anote" id="anote">Not signed in</span>
263272
</div>
264273
</div>
274+
<div class="mclaim" id="mClaim" style="display:none"></div>
265275
</div>
266276
</div>
267277

@@ -585,6 +595,9 @@ <h2>Automotive CSMS Tool Evaluation</h2>
585595
ltd.innerHTML = `<span class="fname">${f.name}</span><span class="fdesc">${f.desc}</span>`;
586596
tr.appendChild(ltd);
587597

598+
// Feature ID slug used as vote key component (computed once per feature)
599+
const featId = sec.id + '__' + f.name.toLowerCase().replace(/[^a-z0-9]+/g,'_');
600+
588601
VENDORS.forEach(v=>{
589602
const td = document.createElement('td');
590603
if(f.type==='info'){
@@ -603,8 +616,28 @@ <h2>Automotive CSMS Tool Evaluation</h2>
603616
pip.className=`spip ${s}`;
604617
pip.textContent=s;
605618
pip.title={F:'Fully Fulfills',P:'Partially Fulfills',N:'Does Not Fulfill',U:'Unverified / N/A'}[s];
606-
pip.onclick=()=>openModal(v.name,f.name,sec.label,s,f.notes?.[v.key]||'No rationale available. Submit feedback to contribute evidence.');
619+
const voteKey = v.key + ':' + featId;
620+
const inVotableGroup = grp.id !== 'org';
621+
pip.onclick=()=>openModal({
622+
vendorKey: v.key, vendorName: v.name,
623+
featName: f.name, featId, voteKey,
624+
secLabel: sec.label, score: s,
625+
rationale: f.notes?.[v.key]||'No rationale available. Submit feedback to contribute evidence.',
626+
inVotableGroup
627+
});
607628
td.appendChild(pip);
629+
// Vote tally badge — shown only for csms/it groups
630+
if(inVotableGroup){
631+
const tally = (window.ASRG_VOTE_SUMMARY||{})[voteKey]||{up:0,down:0};
632+
const vt = document.createElement('span');
633+
vt.className='vtally';
634+
vt.dataset.vk=voteKey;
635+
if(tally.up>0||tally.down>0){
636+
vt.innerHTML=(tally.up>0?`<span class="vt-up">▲${tally.up}</span>`:'')+
637+
(tally.down>0?`<span class="vt-dn">▼${tally.down}</span>`:'');
638+
}
639+
td.appendChild(vt);
640+
}
608641
}
609642
tr.appendChild(td);
610643
});
@@ -628,38 +661,216 @@ <h2>Automotive CSMS Tool Evaluation</h2>
628661
const SLBL={F:'Fully Fulfills',P:'Partially Fulfills',N:'Does Not Fulfill',U:'Unverified / N/A'};
629662
const SNOTE={F:'Contributes +1.0 to domain score',P:'Contributes +0.5 to domain score',N:'Contributes +0.0 to domain score',U:'Excluded from domain score'};
630663
let activeVote=null;
664+
let _modalCtx=null;
665+
let _claimScore=null;
666+
667+
function escHtml(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;')}
631668

632-
function openModal(vendor,feat,sec,score,rationale){
633-
document.getElementById('mTag').textContent=`${vendor} · ${sec}`;
634-
document.getElementById('mName').textContent=feat;
669+
function openModal(ctx){
670+
_modalCtx=ctx;
671+
activeVote=null;
672+
_claimScore=null;
673+
674+
document.getElementById('mTag').textContent=`${ctx.vendorName} · ${ctx.secLabel}`;
675+
document.getElementById('mName').textContent=ctx.featName;
635676
const big=document.getElementById('mBig');
636-
big.textContent=score; big.className=`msbig ${score}`;
637-
document.getElementById('mLbl').textContent=SLBL[score]||score;
638-
document.getElementById('mSub').textContent=SNOTE[score]||'';
639-
document.getElementById('mRat').textContent=rationale;
677+
big.textContent=ctx.score; big.className=`msbig ${ctx.score}`;
678+
document.getElementById('mLbl').textContent=SLBL[ctx.score]||ctx.score;
679+
document.getElementById('mSub').textContent=SNOTE[ctx.score]||'';
680+
document.getElementById('mRat').textContent=ctx.rationale;
640681
document.getElementById('ftxt').value='';
641-
document.getElementById('vCnt').textContent=`${Math.floor(Math.random()*20)+2} community votes`;
642-
activeVote=null;
643-
document.getElementById('vUp').classList.remove('active');
644-
document.getElementById('vDown').classList.remove('active');
645-
document.getElementById('anote').textContent='Not signed in';
682+
683+
// ── Voting section ──────────────────────────────────────────────────────────
684+
const mfb=document.getElementById('mfb');
685+
if(!ctx.inVotableGroup){
686+
mfb.style.display='none';
687+
} else {
688+
mfb.style.display='';
689+
const auth=window.ASRG_AUTH||{};
690+
const summary=(window.ASRG_VOTE_SUMMARY||{})[ctx.voteKey]||{up:0,down:0};
691+
const myVote=(window.ASRG_MY_VOTES||{})[ctx.voteKey];
692+
693+
// Pre-select user's existing vote
694+
document.getElementById('vUp').classList.remove('active');
695+
document.getElementById('vDown').classList.remove('active');
696+
if(myVote){
697+
activeVote=myVote.vote;
698+
document.getElementById(myVote.vote==='up'?'vUp':'vDown').classList.add('active');
699+
document.getElementById('ftxt').value=myVote.note||'';
700+
}
701+
702+
const total=summary.up+summary.down;
703+
document.getElementById('vCnt').textContent=total>0?`▲${summary.up}${summary.down}`:'No votes yet';
704+
705+
// Show submit vs sign-in based on auth state
706+
if(auth.loggedIn){
707+
document.getElementById('bsub').style.display='';
708+
document.getElementById('blogin').style.display='none';
709+
document.getElementById('anote').textContent=auth.displayName||'Signed in';
710+
} else {
711+
document.getElementById('bsub').style.display='none';
712+
document.getElementById('blogin').style.display='';
713+
document.getElementById('anote').textContent='Not signed in';
714+
}
715+
}
716+
717+
// ── Vendor claim section ────────────────────────────────────────────────────
718+
updateClaimSection(ctx);
719+
646720
document.getElementById('overlay').classList.add('open');
647721
}
722+
723+
function updateClaimSection(ctx){
724+
_claimScore=null;
725+
const el=document.getElementById('mClaim');
726+
if(!el) return;
727+
el.innerHTML=''; el.style.display='none';
728+
if(!ctx.inVotableGroup) return;
729+
730+
const auth=window.ASRG_AUTH||{};
731+
const claims=(window.ASRG_VENDOR_CLAIMS||{})[ctx.vendorKey]||{};
732+
const approved=claims[ctx.featId];
733+
const existing=(window.ASRG_MY_CLAIMS||{})[ctx.featId];
734+
735+
let html='';
736+
737+
// Approved vendor response (visible to everyone)
738+
if(approved){
739+
html+=`<div class="mclaim-approved">
740+
<div class="mlbl">Vendor Response</div>
741+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:7px">
742+
<span class="spip ${approved.score}" style="cursor:default;pointer-events:none">${approved.score}</span>
743+
<span style="font-size:11px;color:var(--text2)">${escHtml(SLBL[approved.score]||approved.score)}</span>
744+
</div>
745+
${approved.note?`<div class="mrat" style="border-left-color:var(--blue)">${escHtml(approved.note)}</div>`:''}
746+
</div>`;
747+
}
748+
749+
// Vendor self-assessment form (only visible to the owning vendor)
750+
if(auth.isVendor&&auth.vendorKey===ctx.vendorKey){
751+
const st=existing?existing.status:null;
752+
const stMap={
753+
pending: {bg:'#fef3c7',bd:'#fde68a',cl:'#92400e'},
754+
approved:{bg:'var(--green-bg)',bd:'var(--green-bd)',cl:'var(--green)'},
755+
rejected:{bg:'var(--red-bg)', bd:'var(--red-bd)', cl:'var(--red)'},
756+
};
757+
const sm=st?stMap[st]:null;
758+
const stBadge=sm?`<span style="font-family:var(--mono);font-size:8px;padding:1px 6px;border-radius:3px;background:${sm.bg};border:1px solid ${sm.bd};color:${sm.cl}">${st.toUpperCase()}</span>`:'';
759+
html+=`<div class="mclaim-edit">
760+
<div class="mlbl" style="margin-bottom:9px">Your Self-Assessment ${stBadge}</div>
761+
<div style="display:flex;gap:7px;margin-bottom:10px">
762+
${['F','P','N','U'].map(sc=>`<button onclick="selectClaimScore('${sc}')" class="spip ${sc}" id="claimBtn_${sc}" style="cursor:pointer;width:32px;height:32px;border:2px solid transparent">${sc}</button>`).join('')}
763+
</div>
764+
<textarea id="claimNote" class="ftxt" style="min-height:60px;margin-bottom:8px" placeholder="Explain how your tool meets this criterion — include version or documentation reference.">${existing?escHtml(existing.note||''):''}</textarea>
765+
<button class="bsub" onclick="submitClaim('${escHtml(ctx.featId)}')">Submit for Review</button>
766+
<span style="font-family:var(--mono);font-size:9px;color:var(--text3);margin-left:8px">ASRG will review &amp; approve</span>
767+
</div>`;
768+
}
769+
770+
if(html){
771+
el.innerHTML=html;
772+
el.style.display='';
773+
if(existing&&existing.score) selectClaimScore(existing.score);
774+
}
775+
}
776+
777+
function selectClaimScore(sc){
778+
_claimScore=sc;
779+
['F','P','N','U'].forEach(s=>{
780+
const b=document.getElementById('claimBtn_'+s);
781+
if(b) b.style.outline=s===sc?'2px solid var(--text)':'none';
782+
});
783+
}
784+
785+
async function submitClaim(featId){
786+
const rest=window.ASRG_REST||{};
787+
const note=(document.getElementById('claimNote')||{}).value||'';
788+
if(!_claimScore){alert('Please select a score first.');return;}
789+
try{
790+
const r=await fetch(rest.base+'/claims',{
791+
method:'POST',
792+
headers:{'Content-Type':'application/json','X-WP-Nonce':rest.nonce},
793+
body:JSON.stringify({feature_id:featId,score:_claimScore,note:note||null}),
794+
});
795+
const data=await r.json();
796+
if(r.ok){
797+
window.ASRG_MY_CLAIMS=window.ASRG_MY_CLAIMS||{};
798+
window.ASRG_MY_CLAIMS[featId]={id:data.id,score:_claimScore,note,status:'pending'};
799+
updateClaimSection(_modalCtx);
800+
alert('Submitted for ASRG review. Thank you!');
801+
} else {
802+
alert('Error: '+(data.message||'Could not submit.'));
803+
}
804+
} catch(e){alert('Network error. Please try again.');}
805+
}
806+
648807
function closeModal(){document.getElementById('overlay').classList.remove('open')}
649808
document.getElementById('overlay').addEventListener('click',e=>{if(e.target===document.getElementById('overlay'))closeModal()});
650809
document.addEventListener('keydown',e=>{if(e.key==='Escape')closeModal()});
651810

652811
function doVote(dir){
812+
const auth=window.ASRG_AUTH||{};
813+
if(!auth.loggedIn){doLogin();return;}
653814
activeVote=dir;
654815
document.getElementById('vUp').classList.toggle('active',dir==='up');
655816
document.getElementById('vDown').classList.toggle('active',dir==='down');
656-
document.getElementById('anote').textContent='Sign in to save your vote';
657817
}
658-
function doSubmit(){
659-
if(!activeVote&&!document.getElementById('ftxt').value.trim()){alert('Please cast a vote or add a comment.');return}
660-
doLogin();
818+
819+
async function doSubmit(){
820+
const auth=window.ASRG_AUTH||{};
821+
if(!auth.loggedIn){doLogin();return;}
822+
const ctx=_modalCtx;
823+
const note=document.getElementById('ftxt').value.trim();
824+
if(!activeVote){alert('Please click Agree or Dispute first.');return;}
825+
const rest=window.ASRG_REST||{};
826+
try{
827+
const r=await fetch(rest.base+'/votes',{
828+
method:'POST',
829+
headers:{'Content-Type':'application/json','X-WP-Nonce':rest.nonce},
830+
body:JSON.stringify({feature_id:ctx.featId,vendor_key:ctx.vendorKey,vote:activeVote,note:note||null}),
831+
});
832+
const data=await r.json();
833+
if(r.ok){
834+
// Update in-memory caches
835+
window.ASRG_VOTE_SUMMARY=window.ASRG_VOTE_SUMMARY||{};
836+
window.ASRG_MY_VOTES=window.ASRG_MY_VOTES||{};
837+
const prev=window.ASRG_MY_VOTES[ctx.voteKey];
838+
if(!window.ASRG_VOTE_SUMMARY[ctx.voteKey]) window.ASRG_VOTE_SUMMARY[ctx.voteKey]={up:0,down:0};
839+
const s=window.ASRG_VOTE_SUMMARY[ctx.voteKey];
840+
if(prev) s[prev.vote]=Math.max(0,(s[prev.vote]||0)-1);
841+
if(!data.retracted) s[activeVote]=(s[activeVote]||0)+1;
842+
if(data.retracted){
843+
delete window.ASRG_MY_VOTES[ctx.voteKey];
844+
activeVote=null;
845+
document.getElementById('vUp').classList.remove('active');
846+
document.getElementById('vDown').classList.remove('active');
847+
} else {
848+
window.ASRG_MY_VOTES[ctx.voteKey]={id:data.id,vote:activeVote,note:note||null};
849+
}
850+
// Refresh modal vote count display
851+
const total=s.up+s.down;
852+
document.getElementById('vCnt').textContent=total>0?`▲${s.up}${s.down}`:'No votes yet';
853+
// Refresh tally in the table cell
854+
updateTableTally(ctx.voteKey,s);
855+
} else {
856+
alert('Error: '+(data.message||'Could not save vote.'));
857+
}
858+
} catch(e){alert('Network error. Please try again.');}
859+
}
860+
861+
function updateTableTally(voteKey,tally){
862+
const el=document.querySelector(`.vtally[data-vk="${CSS.escape(voteKey)}"]`);
863+
if(!el) return;
864+
const up=tally?.up||0, dn=tally?.down||0;
865+
el.innerHTML=(up>0?`<span class="vt-up">▲${up}</span>`:'')+
866+
(dn>0?`<span class="vt-dn">▼${dn}</span>`:'');
867+
}
868+
869+
function doLogin(){
870+
const auth=window.ASRG_AUTH||{};
871+
const base=auth.loginUrl||'https://garage.asrg.io';
872+
window.location.href=base+'?redirect_to='+encodeURIComponent(window.location.href);
661873
}
662-
function doLogin(){alert('Redirect to ASRG portal authentication.\n\nReplace with garage.asrg.io SSO endpoint.')}
663874

664875
build();
665876
</script>

0 commit comments

Comments
 (0)