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 - z 0 - 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>
628661const SLBL = { F :'Fully Fulfills' , P :'Partially Fulfills' , N :'Does Not Fulfill' , U :'Unverified / N/A' } ;
629662const 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' } ;
630663let activeVote = null ;
664+ let _modalCtx = null ;
665+ let _claimScore = null ;
666+
667+ function escHtml ( s ) { return String ( s ) . replace ( / & / g, '&' ) . replace ( / < / g, '<' ) . replace ( / > / g, '>' ) . replace ( / " / g, '"' ) }
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 & 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+
648807function closeModal ( ) { document . getElementById ( 'overlay' ) . classList . remove ( 'open' ) }
649808document . getElementById ( 'overlay' ) . addEventListener ( 'click' , e => { if ( e . target === document . getElementById ( 'overlay' ) ) closeModal ( ) } ) ;
650809document . addEventListener ( 'keydown' , e => { if ( e . key === 'Escape' ) closeModal ( ) } ) ;
651810
652811function 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
664875build ( ) ;
665876</ script >
0 commit comments