@@ -45,8 +45,100 @@ <h1 class="mt-4 text-3xl font-semibold leading-tight sm:text-4xl">
4545 showBusinessPlanModal : false ,
4646 showEvaluationModal : false ,
4747 showPartnerEventsModal : false ,
48+ showCsvModal : false ,
4849 activeStep : 0 ,
4950 band : 'band1' ,
51+ roeView : 'partner' , // 'partner' | 'internal'
52+ defaultBandByType : { Registered :'band3' , Preferred :'band2' , Premier :'band1' , MSP :'band1' } ,
53+ simpleROE : {
54+ Registered : {
55+ summary : "You can refer and transact. Co-sell upgrades your margin; order-only stays referral." ,
56+ sourcing : [
57+ { label :"Partner Sourced" , value :"You found the opp + registered it + helped close → Full margin when you drive the sale." } ,
58+ { label :"Recast Sourced (Co-Sell)" , value :"We brought the lead; you help sell → Shared margin." } ,
59+ { label :"Transactional/Referral" , value :"Intro or PO only → Referral margin." }
60+ ] ,
61+ bands : [
62+ { band :"Band 2 – Co-Sell" , when :[ "Host/lead discovery" , "Own next steps/MAP" , "Participate in demo/validation" , "Coordinate quote/security" ] , rule :"Any 2+ qualify" } ,
63+ { band :"Band 3 – Transactional" , when :[ "Intro only or PO processing" ] , rule :"Default if <2 activities above" }
64+ ] ,
65+ renewals :[
66+ "PoR keeps renewal if active (renewal plan ≥90 days out + QBR/health checks)." ,
67+ "If inactive or customer reassigns, Recast may reassign renewal."
68+ ] ,
69+ csv : {
70+ blurb :"Upload event or campaign lists for fast routing. Influence ≠ margin." ,
71+ required :[ "Partner Name" , "Campaign Name/ID" , "Account Name (and Domain)" , "First Name" , "Last Name" , "Email" , "Country" , "Source Type" , "Attribution Intent" , "Requested PoR (Y/N)" ] ,
72+ flow :[
73+ "Create/Update Campaign Members; de-dupe by email; match Account by Domain/Name." ,
74+ "Stamp: Lead Source=Partner Event; Sourcing Partner; Attribution (Primary/Influence)." ,
75+ "Option: 'Create Deal Registration' → PQL → Channel approval in 2 business days."
76+ ]
77+ }
78+ } ,
79+ Preferred : {
80+ summary :"You co-sell with us; consistent activity earns Band 2. Own full cycle → Band 1." ,
81+ sourcing : [
82+ { label :"Partner Sourced" , value :"You found the opp + registered it + helped close → Full margin when you drive the sale." } ,
83+ { label :"Recast Sourced (Co-Sell)" , value :"We brought the lead; you help sell → Shared margin." } ,
84+ { label :"Transactional/Referral" , value :"Intro or PO only → Referral margin." }
85+ ] ,
86+ bands :[
87+ { band :"Band 1 – Full-Cycle" , when :[ "Sourced + registered" , "Owns qual→close with light Recast support" ] , rule :"Full margin" } ,
88+ { band :"Band 2 – Co-Sell" , when :[ "Shared discovery/demo/pricing; active MAP" ] , rule :"Shared margin" }
89+ ] ,
90+ renewals :[ "PoR retains renewal with activity compliance; else may be reassigned." ] ,
91+ csv : {
92+ blurb :"Upload event or campaign lists for fast routing. Influence ≠ margin." ,
93+ required :[ "Partner Name" , "Campaign Name/ID" , "Account Name (and Domain)" , "First Name" , "Last Name" , "Email" , "Country" , "Source Type" , "Attribution Intent" , "Requested PoR (Y/N)" ] ,
94+ flow :[
95+ "Create/Update Campaign Members; de-dupe by email; match Account by Domain/Name." ,
96+ "Stamp: Lead Source=Partner Event; Sourcing Partner; Attribution (Primary/Influence)." ,
97+ "Option: 'Create Deal Registration' → PQL → Channel approval in 2 business days."
98+ ]
99+ }
100+ } ,
101+ Premier : {
102+ summary :"Strategic partner. Default expectation is Band 1 unless co-sell is requested." ,
103+ sourcing : [
104+ { label :"Partner Sourced" , value :"You found the opp + registered it + helped close → Full margin when you drive the sale." } ,
105+ { label :"Recast Sourced (Co-Sell)" , value :"We brought the lead; you help sell → Shared margin." } ,
106+ { label :"Transactional/Referral" , value :"Intro or PO only → Referral margin." }
107+ ] ,
108+ bands :[
109+ { band :"Band 1 – Full-Cycle" , when :[ "Sourced + end-to-end ownership" ] , rule :"Full margin" } ,
110+ { band :"Band 2 – Co-Sell" , when :[ "Recast Sourced or heavier presales help" ] , rule :"Shared margin" }
111+ ] ,
112+ renewals :[ "PoR renewal protection with activity; expansions follow Bands." ] ,
113+ csv : {
114+ blurb :"Upload event or campaign lists for fast routing. Influence ≠ margin." ,
115+ required :[ "Partner Name" , "Campaign Name/ID" , "Account Name (and Domain)" , "First Name" , "Last Name" , "Email" , "Country" , "Source Type" , "Attribution Intent" , "Requested PoR (Y/N)" ] ,
116+ flow :[
117+ "Create/Update Campaign Members; de-dupe by email; match Account by Domain/Name." ,
118+ "Stamp: Lead Source=Partner Event; Sourcing Partner; Attribution (Primary/Influence)." ,
119+ "Option: 'Create Deal Registration' → PQL → Channel approval in 2 business days."
120+ ]
121+ }
122+ } ,
123+ MSP : {
124+ summary :"Managed service delivery; monthly economics; PoR protections per MSP ROE." ,
125+ sourcing :[ { label :"MSP Motion" , value :"You package Recast in services; co-sell on net-new logos." } ] ,
126+ bands :[
127+ { band :"Band 1" , when :[ "MSP drives sale and delivery" ] , rule :"Programmed in MSP price book" } ,
128+ { band :"Band 2" , when :[ "Co-sell with Recast SE/AE" ] , rule :"Shared margin" }
129+ ] ,
130+ renewals :[ "Incumbent MSP retains renewal if active in lifecycle reviews." ] ,
131+ csv : {
132+ blurb :"Upload event or campaign lists for fast routing. Influence ≠ margin. Emphasize multitenant lists." ,
133+ required :[ "Partner Name" , "Campaign Name/ID" , "Account Name (and Domain)" , "First Name" , "Last Name" , "Email" , "Country" , "Source Type" , "Attribution Intent" , "Requested PoR (Y/N)" ] ,
134+ flow :[
135+ "Create/Update Campaign Members; de-dupe by email; match Account by Domain/Name." ,
136+ "Stamp: Lead Source=Partner Event; Sourcing Partner; Attribution (Primary/Influence)." ,
137+ "Option: 'Create Deal Registration' → PQL → Channel approval in 2 business days."
138+ ]
139+ }
140+ }
141+ } ,
50142 exampleACV : 100000 ,
51143 salesCycle : {
52144 stages : {
@@ -483,6 +575,7 @@ <h1 class="mt-4 text-3xl font-semibold leading-tight sm:text-4xl">
483575 if ( STATE . showPartnerEventsModal ) {
484576 html += renderPartnerEventsModal ( ) ;
485577 }
578+ if ( STATE . showCsvModal ) html += renderCsvModal ( ) ;
486579 document . getElementById ( 'app' ) . innerHTML = html ;
487580 lucide . createIcons ( ) ;
488581 addEventListeners ( ) ;
@@ -705,17 +798,100 @@ <h4 class="font-medium mb-2">${r.band} - Up to ${r.upTo}</h4>
705798 return html ;
706799 }
707800 function renderROEContent ( ) {
801+ // MSP already has its own ROE block; keep that path
708802 if ( STATE . partnerType === 'MSP' ) {
709- let html = '<h2 class="text-xl font-medium mb-6 flex items-center gap-3"><i data-lucide="gavel" class="h-6 w-6 stroke-[#2563eb]"></i>Rules of Engagement</h2>' ;
710- html += '<ol class="space-y-6 list-decimal pl-6">' ;
803+ // Add partner/internal toggle even for MSP
804+ return renderROEWithToggle ( renderSimpleROE ( STATE . partnerType ) , renderInternalROE ( ) ) ;
805+ }
806+ return renderROEWithToggle ( renderSimpleROE ( STATE . partnerType ) , renderInternalROE ( ) ) ;
807+ }
808+ function renderROEWithToggle ( partnerHtml , internalHtml ) {
809+ const toggle = `
810+ <div class="flex items-center justify-between mb-4">
811+ <h2 class="text-xl font-medium flex items-center gap-3">
812+ <i data-lucide="gavel" class="h-6 w-6 stroke-[#2563eb]"></i>Rules of Engagement
813+ </h2>
814+ <div class="inline-flex rounded-full bg-gray-100 p-1">
815+ <button data-roeview="partner" class="px-3 py-1 text-sm rounded-full ${ STATE . roeView === 'partner' ?'bg-white shadow' :'' } ">Partner-facing</button>
816+ <button data-roeview="internal" class="px-3 py-1 text-sm rounded-full ${ STATE . roeView === 'internal' ?'bg-white shadow' :'' } ">Internal</button>
817+ </div>
818+ </div>` ;
819+ const body = STATE . roeView === 'partner' ? partnerHtml : internalHtml ;
820+ return toggle + body ;
821+ }
822+ function renderSimpleROE ( type ) {
823+ const d = STATE . simpleROE [ type ] ;
824+ const defBand = STATE . defaultBandByType [ type ] || 'band2' ;
825+ const bandMeta = STATE . salesCycle . bandMeta [ defBand ] ;
826+
827+ const card = ( title , inner ) => `
828+ <div class="rounded-xl bg-white shadow-md hover:shadow-lg transition overflow-hidden">
829+ <h3 class="text-lg font-semibold p-5 border-b border-[#E2E8F0] bg-gray-50 flex items-center gap-2">
830+ <i data-lucide="file-text" class="h-5 w-5 stroke-[#2563eb]"></i>${ title }
831+ </h3>
832+ <div class="p-5">${ inner } </div>
833+ </div>` ;
834+
835+ const list = ( items ) => `<ul class="space-y-2">${ items . map ( i => `
836+ <li class="flex items-start gap-3">
837+ <span class="mt-1 bg-[#2563eb] rounded-full p-1"><i data-lucide="check" class="h-3 w-3 stroke-white"></i></span>
838+ <span class="text-gray-700 leading-relaxed">${ typeof i === 'string' ?i :`<strong>${ i . label } :</strong> ${ i . value } ` } </span>
839+ </li>` ) . join ( '' ) } </ul>`;
840+
841+ const bandsTable = `
842+ <div class="overflow-x-auto">
843+ <table class="min-w-full divide-y divide-[#E2E8F0]">
844+ <thead><tr>
845+ <th class="px-4 py-2 text-left text-sm font-semibold">Band</th>
846+ <th class="px-4 py-2 text-left text-sm font-semibold">When</th>
847+ <th class="px-4 py-2 text-left text-sm font-semibold">Rule</th>
848+ </tr></thead>
849+ <tbody class="divide-y divide-[#E2E8F0]">
850+ ${ d . bands . map ( b => `
851+ <tr>
852+ <td class="px-4 py-3 text-sm font-medium">${ b . band } </td>
853+ <td class="px-4 py-3 text-sm">
854+ <ul class="list-disc pl-5 space-y-1">${ ( b . when || [ ] ) . map ( w => `<li>${ w } </li>` ) . join ( '' ) } </ul>
855+ </td>
856+ <td class="px-4 py-3 text-sm">${ b . rule || '' } </td>
857+ </tr>` ) . join ( '' ) }
858+ </tbody>
859+ </table>
860+ </div>` ;
861+
862+ const csvBlock = `
863+ <p class="text-gray-700 leading-relaxed mb-3">${ d . csv . blurb } </p>
864+ <div class="grid md:grid-cols-3 gap-4">
865+ <div><div class="font-medium mb-1">Required Columns</div>${ list ( d . csv . required ) } </div>
866+ <div><div class="font-medium mb-1">What Happens</div>${ list ( d . csv . flow ) } </div>
867+ <div class="flex items-start">
868+ <button id="csvSopBtn" class="rounded-full bg-[#2563eb] hover:bg-[#1d4ed8] px-4 py-2 text-sm text-white shadow-sm">CSV Upload SOP</button>
869+ </div>
870+ </div>` ;
871+
872+ return `
873+ <div class="space-y-6">
874+ <div class="flex flex-wrap items-center gap-2">
875+ <span class="px-3 py-1 text-xs rounded-2xl ${ bandMeta . color } ">${ bandMeta . name } </span>
876+ <span class="text-xs text-gray-600">Default expectation for ${ type } </span>
877+ </div>
878+ ${ card ( "Sourcing & Attribution" , list ( d . sourcing ) ) }
879+ ${ card ( "Registration Bands (What You Earn)" , bandsTable ) }
880+ ${ card ( "Renewals" , list ( d . renewals ) ) }
881+ ${ card ( "Uploading Event/Co-Marketing Leads" , csvBlock ) }
882+ </div>` ;
883+ }
884+ function renderInternalROE ( ) {
885+ if ( STATE . partnerType === 'MSP' ) {
886+ let html = '<ol class="space-y-6 list-decimal pl-6">' ;
711887 STATE . msp . roe . forEach ( rule => {
712888 html += `<li class="text-gray-700 leading-relaxed">${ rule . replace ( / \n / g, '<br>' ) } </li>` ;
713889 } ) ;
714890 html += '</ol>' ;
715891 return html ;
716892 } else {
717- let html = '<h2 class="text-xl font-medium mb-6 flex items-center gap-3"><i data-lucide="gavel" class="h-6 w-6 stroke-[#2563eb]"></i>Rules of Engagement</h2>' ;
718- html + = '<div class="space-y-6">' ;
893+ // reuse existing blocks
894+ let html = '<div class="space-y-6">' ;
719895 html += renderROESection ( 'Rules of Engagement (ROE)' , renderPrinciplesTable ( ) ) ;
720896 html += renderROESection ( 'Margin Bands & Definitions' , renderBandsTable ( ) ) ;
721897 html += renderROESection ( 'Partner Margin Governance' , renderGovernanceTable ( ) ) ;
@@ -1107,6 +1283,29 @@ <h2 class="text-xl font-medium mb-6 flex items-center gap-3"><i data-lucide="sca
11071283 html += '</section>' ;
11081284 return html ;
11091285 }
1286+ function renderCsvModal ( ) {
1287+ const sample = "Partner Name,Campaign Name,Account Name,Domain,First Name,Last Name,Email,Country,Source Type,Attribution Intent,Requested PoR\nAcme Partner,2025-03 Webinar,Contoso,contoso.com,Jane,Doe,jane@contoso.com,US,Webinar,Partner Sourced,Yes" ;
1288+ return `
1289+ <div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 px-4" id="csvModal">
1290+ <div class="relative max-w-3xl w-full bg-white rounded-xl shadow-2xl p-8 max-h-[90vh] overflow-y-auto">
1291+ <button class="absolute top-4 right-4 text-gray-600 hover:text-gray-900" id="closeCsvModal"><i data-lucide="x" class="h-6 w-6"></i></button>
1292+ <h2 class="text-xl font-medium mb-4 flex items-center gap-3"><i data-lucide="upload" class="h-6 w-6 stroke-[#2563eb]"></i>CSV Upload SOP</h2>
1293+ <ol class="list-decimal pl-6 space-y-2 text-gray-700">
1294+ <li>Download the template CSV.</li>
1295+ <li>Fill required columns; one row per contact.</li>
1296+ <li>Choose “Create Deal Registration” for qualified rows.</li>
1297+ <li>Channel approves within 2 business days (PoR + Band applied).</li>
1298+ </ol>
1299+ <div class="mt-4">
1300+ <pre class="bg-gray-50 p-3 rounded-lg text-xs overflow-auto">${ sample } </pre>
1301+ </div>
1302+ <div class="mt-4 flex gap-2">
1303+ <button class="rounded-full bg-[#2563eb] text-white px-4 py-2 text-sm" id="downloadCsvTpl">Download Template</button>
1304+ <button class="rounded-full border px-4 py-2 text-sm" id="closeCsvModal2">Close</button>
1305+ </div>
1306+ </div>
1307+ </div>` ;
1308+ }
11101309 function addEventListeners ( ) {
11111310 document . getElementById ( 'partnerType' ) . addEventListener ( 'change' , ( e ) => {
11121311 STATE . partnerType = e . target . value ;
@@ -1210,22 +1409,52 @@ <h2 class="text-xl font-medium mb-6 flex items-center gap-3"><i data-lucide="sca
12101409 render ( ) ;
12111410 } ) ;
12121411 }
1213- const modals = document . querySelectorAll ( '#businessPlanModal, #partnerEventsModal, #evaluationModal' ) ;
1412+ document . querySelectorAll ( '[data-roeview]' ) . forEach ( btn => {
1413+ btn . addEventListener ( 'click' , ( ) => {
1414+ STATE . roeView = btn . dataset . roeview ;
1415+ render ( ) ;
1416+ } ) ;
1417+ } ) ;
1418+ const csvBtn = document . getElementById ( 'csvSopBtn' ) ;
1419+ if ( csvBtn ) {
1420+ csvBtn . addEventListener ( 'click' , ( ) => {
1421+ STATE . showCsvModal = true ;
1422+ render ( ) ;
1423+ } ) ;
1424+ }
1425+ const closeCsv = document . getElementById ( 'closeCsvModal' ) ;
1426+ const closeCsv2 = document . getElementById ( 'closeCsvModal2' ) ;
1427+ if ( closeCsv || closeCsv2 ) {
1428+ [ closeCsv , closeCsv2 ] . filter ( Boolean ) . forEach ( b => b . addEventListener ( 'click' , ( ) => { STATE . showCsvModal = false ; render ( ) ; } ) ) ;
1429+ }
1430+ const dl = document . getElementById ( 'downloadCsvTpl' ) ;
1431+ if ( dl ) {
1432+ dl . addEventListener ( 'click' , ( ) => {
1433+ const csv = `Partner Name,Campaign Name,Account Name,Domain,First Name,Last Name,Email,Country,Source Type,Attribution Intent,Requested PoR\n` ;
1434+ const blob = new Blob ( [ csv ] , { type :'text/csv' } ) ;
1435+ const url = URL . createObjectURL ( blob ) ;
1436+ const a = document . createElement ( 'a' ) ; a . href = url ; a . download = 'recast_partner_upload_template.csv' ; a . click ( ) ;
1437+ URL . revokeObjectURL ( url ) ;
1438+ } ) ;
1439+ }
1440+ const modals = document . querySelectorAll ( '#businessPlanModal, #partnerEventsModal, #evaluationModal, #csvModal' ) ;
12141441 modals . forEach ( modal => {
12151442 modal . addEventListener ( 'click' , ( e ) => {
12161443 if ( e . target === modal ) {
12171444 STATE . showBusinessPlanModal = false ;
12181445 STATE . showPartnerEventsModal = false ;
12191446 STATE . showEvaluationModal = false ;
1447+ STATE . showCsvModal = false ;
12201448 render ( ) ;
12211449 }
12221450 } ) ;
12231451 } ) ;
12241452 document . addEventListener ( 'keydown' , ( e ) => {
1225- if ( e . key === 'Escape' && ( STATE . showBusinessPlanModal || STATE . showPartnerEventsModal || STATE . showEvaluationModal ) ) {
1453+ if ( e . key === 'Escape' && ( STATE . showBusinessPlanModal || STATE . showPartnerEventsModal || STATE . showEvaluationModal || STATE . showCsvModal ) ) {
12261454 STATE . showBusinessPlanModal = false ;
12271455 STATE . showPartnerEventsModal = false ;
12281456 STATE . showEvaluationModal = false ;
1457+ STATE . showCsvModal = false ;
12291458 render ( ) ;
12301459 }
12311460 } ) ;
0 commit comments