JOURNEY OF MANY
Private / Command Access

RUN THE
JOURNEY.

The operating system behind Journey of Many. One place for the work, people, opportunities, money and movement that matter.

Authenticated owner account
ACTIVE'; const [tasks,opps,posts,biz]=await Promise.all([count('command_tasks','owner_id=eq.'+user.id+'&status=neq.done'),count('command_opportunities','owner_id=eq.'+user.id),count('community_posts','visibility=eq.public'),count('command_businesses','owner_id=eq.'+user.id)]);$('mTasks').textContent=tasks;$('mOpps').textContent=opps;$('mPosts').textContent=posts;$('mBusinesses').textContent=biz; await Promise.all([loadTasks(),loadOpps(),loadCommunity(),loadActivity()]);$('focusTitle').textContent=tasks?'Move what matters.':'Build the system.';$('focusCopy').textContent=tasks?tasks+' open priorities are currently waiting for movement. Start with the highest-impact item.':'Your operating layer is live. Add the first priority, opportunity or business and JOM will start building your command view.'} async function loadTasks(){const data=await rest('command_tasks','owner_id=eq.'+user.id+'&order=created_at.desc&limit=40');const open=data.filter(x=>x.status!=='done');$('taskCount').textContent=data.length+' records';const html=open.length?open.map(x=>'
'+esc(x.title)+'
'+esc(x.description||'No description')+(x.due_at?' · Due '+new Date(x.due_at).toLocaleDateString():'')+'
'+esc(x.priority||'normal')+'
').join(''):'
No open priorities. The board is clear.
';$('taskList').innerHTML=html;$('todayList').innerHTML=open.slice(0,5).map(x=>'
'+esc(x.title)+'
'+esc(x.description||'Keep it moving')+'
'+esc(x.priority||'normal')+'
').join('')||'
Nothing urgent here.
'} function renderPipelineStats(data){let s=$('pipelineStats');if(!s){s=document.createElement('div');s.id='pipelineStats';s.className='cc-command-strip';s.style.cssText='margin-bottom:18px;grid-template-columns:repeat(5,1fr)';const card=document.querySelector('#sec-pipeline .cc-card');if(card)card.parentNode.insertBefore(s,card)}const stages=['new','contacted','proposal','won','lost'];const labels=['New','Contacted','Proposal','Won','Lost'];s.innerHTML=stages.map((stage,i)=>'
'+data.filter(x=>x.stage===stage).length+'
'+labels[i]+'
').join('')}\nasync function loadOpps(){const data=await rest('command_opportunities','owner_id=eq.'+user.id+'&order=created_at.desc&limit=30');renderPipelineStats(data);const html=data.length?data.map(x=>'
'+esc(x.name)+'
'+esc(x.contact_name||'No contact')+(x.next_action?' · '+esc(x.next_action):'')+(x.value?' · '+esc(x.currency||'NZD')+' '+Number(x.value).toLocaleString():'')+'
'+esc(x.stage||'new')+'
').join(''):'
No opportunities yet.
';$('oppList').innerHTML=html;$('miniOpps').innerHTML=data.slice(0,4).map(x=>'
'+esc(x.name)+'
'+esc(x.next_action||x.contact_name||'Next action not set')+'
'+esc(x.stage||'new')+'
').join('')||'
Pipeline is clear.
'} async function loadCommunity(){const data=await rest('community_posts','select=*,profiles(display_name,username,avatar_url)&visibility=eq.public&order=created_at.desc&limit=20');const ids=data.map(x=>x.id);let likes=[];if(ids.length){try{likes=await rest('community_likes','select=post_id,user_id&post_id=in.('+ids.join(',')+')')}catch(e){}}const likeMap={};likes.forEach(x=>{likeMap[x.post_id]=(likeMap[x.post_id]||0)+1});$('communityList').innerHTML=data.length?data.map(x=>{const p=x.profiles||{};const name=p.display_name||p.username||'JOM Member';return '
'+esc(name)+'
MEMBER
'+esc(x.body)+'
'+new Date(x.created_at).toLocaleString()+' · '+(likeMap[x.id]||0)+' likes
'}).join(''):'
No community activity yet. Be the first to post.
'} async function loadActivity(){try{const data=await rest('command_activity','owner_id=eq.'+user.id+'&order=created_at.desc&limit=8');$('activityList').innerHTML=data.length?data.map(x=>'
'+esc(x.title)+'
'+esc(x.description||x.activity_type||'Activity')+'
'+new Date(x.created_at).toLocaleDateString()+'
').join(''):'
System activity will appear here.
'}catch(e){$('activityList').innerHTML='
Activity layer ready.
'}} async function enter(s){session=s;localStorage.setItem(SK,JSON.stringify(s));try{const owners=await rest('command_owners','select=user_id&user_id=eq.'+encodeURIComponent(s.user.id)+'&limit=1');if(!owners.length){localStorage.removeItem(SK);session=null;$('ccApp').classList.add('hidden');$('ccAuth').classList.remove('hidden');msg('ccLoginMsg','This account is authenticated but is not authorised for the JOM Command Center.');return;}$('ccAuth').classList.add('hidden');$('ccApp').classList.remove('hidden');await load()}catch(e){localStorage.removeItem(SK);session=null;$('ccApp').classList.add('hidden');$('ccAuth').classList.remove('hidden');msg('ccLoginMsg','Command Center access could not be verified. Please try again.')}} document.querySelectorAll('.cc-nav button').forEach(b=>b.onclick=()=>showSec(b.dataset.sec));document.querySelectorAll('[data-sec-jump]').forEach(b=>b.onclick=()=>showSec(b.dataset.secJump));$('ccRefresh').onclick=()=>load().then(()=>toast('Command Center refreshed')).catch(e=>toast(e.message));$('ccMobileMenu').onclick=()=>{const nav=document.querySelector('.cc-side');if(nav)nav.style.display=nav.style.display==='block'?'none':'block'}; $('ccShowSignup').onclick=()=>{$('ccLoginPanel').classList.add('hidden');$('ccSignupPanel').classList.remove('hidden')};$('ccShowLogin').onclick=()=>{$('ccSignupPanel').classList.add('hidden');$('ccLoginPanel').classList.remove('hidden')}; $('ccSignup').onclick=async()=>{try{const email=$('ccSignupEmail').value.trim(),password=$('ccSignupPassword').value,name=$('ccName').value.trim();if(password.length<8)throw Error('Password must be at least 8 characters.');const d=await auth('/auth/v1/登録',{email,password,data:{display_name:name},redirect_to:'https://www.journeyofmany.com/jom-owner-command-center/'});if(!d.access_token){msg('ccSignupMsg','Account created. Check your email to confirm it, then return here to sign in.');return}await enter({access_token:d.access_token,refresh_token:d.refresh_token,user:d.user})}catch(e){msg('ccSignupMsg',e.message)}}; $('ccLogin').onclick=async()=>{try{const d=await auth('/auth/v1/token?grant_type=password',{email:$('ccEmail').value.trim(),password:$('ccPassword').value});await enter({access_token:d.access_token,refresh_token:d.refresh_token,user:d.user})}catch(e){msg('ccLoginMsg',e.message)}}; $('ccSignout').onclick=()=>{localStorage.removeItem(SK);location.reload()};document.querySelectorAll('[data-close-modal]').forEach(b=>b.onclick=()=>b.closest('[id$="Modal"]').classList.add('hidden')); document.querySelectorAll('[data-modal]').forEach(b=>b.onclick=()=>{($(b.dataset.modal==='task'?'ccTaskModal':b.dataset.modal==='opp'?'ccOppModal':'ccPostModal')).classList.remove('hidden')}); $('savePost').onclick=async()=>{const body=$('postBody').value.trim();if(!body)return toast('Write something first.');try{const r=await fetch(API+'/rest/v1/community_posts',{method:'POST',headers:hdr(session.access_token),body:JSON.stringify({author_id:user.id,body,post_type:'post',visibility:'public'})});if(!r.ok)throw Error((await r.json()).message||'Could not publish post');$('ccPostModal').classList.add('hidden');$('postBody').value='';toast('Post published');await load()}catch(e){toast(e.message)}}; document.addEventListener('click',async e=>{const b=e.target.closest('[data-like-post]');if(!b||!session)return;try{const post=b.dataset.likePost;const existing=await rest('community_likes','select=post_id&post_id=eq.'+encodeURIComponent(post)+'&user_id=eq.'+encodeURIComponent(user.id));if(existing.length){const r=await fetch(API+'/rest/v1/community_likes?post_id=eq.'+encodeURIComponent(post)+'&user_id=eq.'+encodeURIComponent(user.id),{method:'DELETE',headers:hdr(session.access_token)});if(!r.ok)throw Error('Could not remove like');b.textContent='Like'}else{const r=await fetch(API+'/rest/v1/community_likes',{method:'POST',headers:hdr(session.access_token),body:JSON.stringify({post_id:post,user_id:user.id})});if(!r.ok)throw Error((await r.json()).message||'Could not like post');b.textContent='Liked'}await loadCommunity()}catch(err){toast(err.message)}});\n$('saveTask').onclick=async()=>{const title=$('taskTitle').value.trim();if(!title)return toast('Add a task title.');try{await rest('command_tasks','',{prefer:'return=representation'});const r=await fetch(API+'/rest/v1/command_tasks',{method:'POST',headers:hdr(session.access_token),body:JSON.stringify({owner_id:user.id,title,description:$('taskDesc').value.trim(),priority:$('taskPriority').value,due_at:$('taskDue').value?new Date($('taskDue').value+'T12:00:00').toISOString():null})});if(!r.ok)throw Error((await r.json()).message||'Could not create task');$('ccTaskModal').classList.add('hidden');$('taskTitle').value='';$('taskDesc').value='';$('taskDue').value='';toast('Priority added');await load()}catch(e){toast(e.message)}}; $('saveOpp').onclick=async()=>{const name=$('oppName').value.trim();if(!name)return toast('Add an opportunity name.');try{const r=await fetch(API+'/rest/v1/command_opportunities',{method:'POST',headers:hdr(session.access_token),body:JSON.stringify({owner_id:user.id,name,contact_name:$('oppContact').value.trim(),contact_email:$('oppEmail').value.trim()||null,value:Number($('oppValue').value||0),stage:$('oppStage').value,next_action:$('oppNext').value.trim()})});if(!r.ok)throw Error((await r.json()).message||'Could not create opportunity');$('ccOppModal').classList.add('hidden');['oppName','oppContact','oppEmail','oppValue','oppNext'].forEach(id=>$(id).value='');toast('Opportunity added');await load()}catch(e){toast(e.message)}}; document.addEventListener('click',async e=>{const o=e.target.closest('[data-advance-opp]');if(o&&session){const stages=['new','contacted','proposal','won','lost'];try{const current=await rest('command_opportunities','id=eq.'+encodeURIComponent(o.dataset.advanceOpp));if(!current.length)return;const i=stages.indexOf(current[0].stage||'new');const next=stages[Math.min(i+1,stages.length-1)];if(next===current[0].stage)return toast('Opportunity is already at the final stage.');o.disabled=true;const r=await fetch(API+'/rest/v1/command_opportunities?id=eq.'+encodeURIComponent(o.dataset.advanceOpp),{method:'PATCH',headers:{...hdr(session.access_token),'Prefer':'return=minimal'},body:JSON.stringify({stage:next})});if(!r.ok)throw Error((await r.json()).message||'Could not advance opportunity');toast('Opportunity moved to '+next);await load()}catch(err){toast(err.message);o.disabled=false}return}const b=e.target.closest('[data-task-done]');if(!b||!session)return;const id=b.dataset.taskDone;b.disabled=true;try{const r=await fetch(API+'/rest/v1/command_tasks?id=eq.'+encodeURIComponent(id),{method:'PATCH',headers:{...hdr(session.access_token),'Prefer':'return=minimal'},body:JSON.stringify({status:'done',completed_at:new Date().toISOString()})});if(!r.ok)throw Error((await r.json()).message||'Could not complete task');toast('Task completed');await load()}catch(err){toast(err.message);b.disabled=false}});const saved=localStorage.getItem(SK);if(saved){try{session=JSON.parse(saved);user=session.user;$('ccAuth').classList.add('hidden');$('ccApp').classList.remove('hidden');load().catch(()=>{localStorage.removeItem(SK);location.reload()})}catch(e){localStorage.removeItem(SK)}} async function loadProfileSubmissions(){const box=$('ccProfileSubmissions');if(!box||!session)return;box.innerHTML='
Loading profile submissions...
';try{const rows=await rest('profile_submissions','status=eq.pending&order=created_at.desc&limit=50');if(!rows.length){box.innerHTML='
ALL CLEAR
There are no pending profile submissions.
';return}box.innerHTML=rows.map(s=>'
'+esc(s.display_name||s.real_name||'Unnamed')+'
'+esc((s.profile_type||'profile').toUpperCase())+' · '+esc(s.email||'No email')+' · '+esc([s.city,s.country].filter(Boolean).join(', ')||'Location not supplied')+'
'+esc(s.bio||'No bio supplied')+'
').join('')}catch(e){box.innerHTML='
'+esc(e.message||'Could not load submissions.')+'
'}}async function approveProfileSubmission(id){const s=(await rest('profile_submissions','id=eq.'+encodeURIComponent(id)+'&limit=1'))[0];if(!s)return toast('Submission not found.');const base=(s.display_name||s.real_name||'member').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'').slice(0,36)||'member';const username=base+'-'+id.slice(0,8);try{const p=await rest('profiles','select=id&username=eq.'+encodeURIComponent(username)+'&limit=1');if(p.length)throw new Error('A profile username collision occurred. Try approving again.');const created=await rest('profiles','',{method:'POST',body:JSON.stringify({id:crypto.randomUUID(),username,display_name:s.display_name||s.real_name||'JOM Member',bio:s.bio||'',location:[s.city,s.country].filter(Boolean).join(', '),disciplines:Array.isArray(s.disciplines)?s.disciplines:[],journey_stage:'new',is_public:true})});const profile=created[0];await rest('profile_submissions','id=eq.'+encodeURIComponent(id),{method:'PATCH',body:JSON.stringify({status:'approved',linked_profile_id:profile.id,reviewed_at:new Date().toISOString()})});toast('Profile approved');loadProfileSubmissions()}catch(e){toast(e.message||'Could not approve profile.')}}async function rejectProfileSubmission(id){try{await rest('profile_submissions','id=eq.'+encodeURIComponent(id),{method:'PATCH',body:JSON.stringify({status:'rejected',reviewed_at:new Date().toISOString()})});toast('Submission rejected');loadProfileSubmissions()}catch(e){toast(e.message||'Could not reject submission.')}}document.addEventListener('click',e=>{const a=e.target.closest('[data-approve-profile]');if(a){a.disabled=true;approveProfileSubmission(a.dataset.approveProfile)}const r=e.target.closest('[data-reject-profile]');if(r){r.disabled=true;rejectProfileSubmission(r.dataset.rejectProfile)}});})();