Tailwind Upgrade
This commit is contained in:
+250
-170
@@ -4,170 +4,107 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1" maximum-scale="1" />
|
<meta name="viewport" content="width=device-width,initial-scale=1" maximum-scale="1" />
|
||||||
<title>Job Info — Vanilla JS</title>
|
<title>Job Info — Vanilla JS</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||||
<script src="auth.js"></script>
|
<script src="auth.js"></script>
|
||||||
<style>
|
|
||||||
|
|
||||||
/* Basic layout */
|
|
||||||
:root { --accent:#0d6efd; --muted:#6b7280; --card-bg:#ffffff; --card-border:#d1e7ff; }
|
|
||||||
body{font-family:Inter,Arial,Helvetica,sans-serif;background:#f3f4f6;margin:0;color:#111; overflow:hidden; touch-action: pan-y; height:100vh; display:flex; flex-direction:column;}
|
|
||||||
.wrap{max-width:800px;box-sizing:border-box;margin:0 auto;padding:16px;font-size:17px;display:flex;flex-direction:column;flex:1;overflow:hidden;}
|
|
||||||
h1{margin:0 0 12px;text-align:center;color:var(--accent);font-size:33px;font-weight:700;}
|
|
||||||
|
|
||||||
/* Progress */
|
|
||||||
.progress-wrap{width:100%;background:#e6edf7;height:6px;border-radius:4px;overflow:hidden;margin-bottom:12px;}
|
|
||||||
.progress-bar{height:100%;width:0;background:var(--accent);transition:width 0.3s ease-out;}
|
|
||||||
|
|
||||||
/* Search + filter toggle */
|
|
||||||
#searchBox{width:100%;padding:10px;border-radius:8px;border:1px solid #d1d5db;margin-bottom:8px;box-sizing:border-box;font-size: 18px;}
|
|
||||||
#searchBox::placeholder{color:#9ca3af;}
|
|
||||||
#filterToggle{background:var(--accent);color:#fff;padding:8px 10px;border-radius:8px;border:none;cursor:pointer;margin-bottom:8px;display:inline-block;max-width:110px;}
|
|
||||||
|
|
||||||
/* Filters panel */
|
|
||||||
#filtersPanel{display:none;background:#fff;border:1px solid #e5e7eb;border-radius:8px;padding:10px;margin-bottom:10px;}
|
|
||||||
.filter-group{margin-bottom:8px;font-size:14px;}
|
|
||||||
.filter-group label{margin-right:8px;cursor:pointer;color:var(--muted);}
|
|
||||||
|
|
||||||
/* Results/cards */
|
|
||||||
#results{display:flex;flex-direction:column;gap:12px;flex:1;overflow:auto;padding:0;}
|
|
||||||
.card{background:var(--card-bg);border-radius:10px;padding:10px;border:1px solid #e6f0ff;box-shadow:0 6px 14px rgba(14,30,37,0.04);cursor:pointer;position:relative;display:flex;flex-direction:column;justify-content:center;box-sizing:border-box;gap:4px;}
|
|
||||||
.card-row{display:flex;align-items:center;gap:8px;font-size:18px;line-height:1.2;margin-bottom:2px;}
|
|
||||||
.card-label{font-weight:700;color:#374151;min-width:72px;white-space:nowrap;}
|
|
||||||
.card-info{color:#111;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;}
|
|
||||||
.active-dot{position:absolute;top:10px;right:10px;width:14px;height:14px;border-radius:999px;border:2px solid #fff;box-shadow:0 1px 2px rgba(0,0,0,0.12);}
|
|
||||||
|
|
||||||
/* Modal */
|
|
||||||
#detailModal{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);align-items:flex-start;justify-content:center;padding-top:2vh;z-index:999;}
|
|
||||||
#detailBox{margin-block-start:0;margin-block-end:0;background:#fff;border-radius:10px;padding:2px 12px 6px 10px;max-width:380px;width:92%;max-height:96vh;overflow:auto;box-sizing:border-box;}
|
|
||||||
.h2{margin-top:0; margin-bottom:2px;margin-block-end:0;}
|
|
||||||
.detail-table{margin-top: 0;width:100%;border-collapse:collapse;margin-bottom:4px;}
|
|
||||||
.detail-table td{border:1px solid #eef2ff;padding:0px 6px 6px 6px;font-size:13px;vertical-align:top;}
|
|
||||||
.detail-table td.label{width:40%;background:#fbfdff;font-weight:700;color:#374151;}
|
|
||||||
#jobFolderBtn{width:100%;padding:10px 12px;border-radius:8px;border:none;color:#fff;cursor:pointer;font-size:17px;font-weight:700;margin-bottom:10px;transition:background 0.2s;}
|
|
||||||
#jobFolderBtn.active{background:var(--accent);}
|
|
||||||
#jobFolderBtn.active:hover{background:#0b5ed7;}
|
|
||||||
#jobFolderBtn:disabled{background:#4b5563;cursor:not-allowed;opacity:0.7;}
|
|
||||||
|
|
||||||
/* Notes */
|
|
||||||
#jobNotes{min-height:120px;max-height:200px;overflow:auto;padding:4px;border:1px solid #4b5563;border-radius:4px;font-size:11px;background:transparent;white-space:pre-wrap;word-wrap:break-word;color:#4b5563;}
|
|
||||||
#newNote{min-height:56px;max-height:120px;overflow:auto;padding:8px;border:1px solid #4b5563;border-radius:8px;font-size:16px;background:transparent;margin-top:6px;}
|
|
||||||
.toolbar{display:flex;flex-wrap:wrap;margin-bottom:3px;margin-top:5px} /* small space below toolbar */
|
|
||||||
.toolbar button{
|
|
||||||
height:28px;
|
|
||||||
width:32px;
|
|
||||||
font-size:12px;
|
|
||||||
padding:0 4px;
|
|
||||||
border-radius:6px;
|
|
||||||
border:1px solid #ccc;
|
|
||||||
background:#ccc;
|
|
||||||
color:#111;
|
|
||||||
cursor:pointer;
|
|
||||||
display:flex;
|
|
||||||
align-items:center;
|
|
||||||
justify-content:center;
|
|
||||||
margin: 1px;
|
|
||||||
}
|
|
||||||
.toolbar button#tbClear{
|
|
||||||
width:auto;
|
|
||||||
padding:4px 8px;
|
|
||||||
background:#9ca3af; /* slightly darker gray */
|
|
||||||
color:#fff;
|
|
||||||
}
|
|
||||||
.toolbar button.active{
|
|
||||||
background:#d1e7ff;
|
|
||||||
color:#111;
|
|
||||||
border-color:#0d6efd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-actions{display:flex;gap:8px;margin-top:6px;margin-bottom:50px;}
|
|
||||||
.btn{padding:8px 10px;border-radius:8px;border:none;color:#fff;cursor:pointer;}
|
|
||||||
.btn.green{background:#16a34a;} .btn.gray{background:#6b7280;} .btn.red{background:#dc2626;}
|
|
||||||
|
|
||||||
/* Footer */
|
|
||||||
.footer{text-align:right;font-size:12px;color:var(--muted);margin-top:20px;padding-top:12px;border-top:1px solid #e5e7eb;}
|
|
||||||
|
|
||||||
/* responsive small */
|
|
||||||
@media (max-width:800px){ .wrap{max-width:100%;} }
|
|
||||||
@media (max-width:380px){ .wrap{padding:12px;} .card{height:98px;} }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="font-sans bg-gray-100 m-0 text-gray-900 overflow-hidden touch-pan-y h-screen flex flex-col">
|
||||||
<div class="wrap">
|
<div class="max-w-full md:max-w-2xl mx-auto p-4 sm:p-3 text-[17px] flex flex-col flex-1 overflow-hidden">
|
||||||
<h1>Job Info</h1>
|
<h1 class="m-0 mb-3 text-center text-blue-600 text-[33px] font-bold">Job Info</h1>
|
||||||
|
|
||||||
<div class="progress-wrap">
|
<div class="w-full bg-blue-50 h-1.5 rounded overflow-hidden mb-3">
|
||||||
<div id="progressBar" class="progress-bar"></div>
|
<div id="progressBar" class="h-full w-0 bg-blue-600 transition-all duration-300 ease-out"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input id="searchBox" placeholder="Search jobs (just start typing)">
|
<input id="searchBox" placeholder="Search jobs (just start typing)" class="w-full p-2.5 rounded-lg border border-gray-300 mb-2 text-lg placeholder:text-gray-400">
|
||||||
|
|
||||||
<button id="filterToggle">Show Filters</button>
|
<button id="filterToggle" class="bg-blue-600 text-white py-2 px-2.5 rounded-lg border-none cursor-pointer mb-2 inline-block min-w-[140px]">Show Filters</button>
|
||||||
|
|
||||||
<div id="filtersPanel">
|
<div id="filtersPanel" class="hidden bg-white border border-gray-200 rounded-lg p-2.5 mb-2.5">
|
||||||
<div class="filter-group">
|
<div class="mb-2 text-sm">
|
||||||
<strong>Division</strong><br/>
|
<strong>Division</strong><br/>
|
||||||
<label><input type="checkbox" class="filter-division" value="C#"> Commercial</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="C#"> Commercial</label>
|
||||||
<label><input type="checkbox" class="filter-division" value="R#"> Residential</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="R#"> Residential</label>
|
||||||
<label><input type="checkbox" class="filter-division" value="S#"> Small Jobs</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-division" value="S#"> Small Jobs</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-group">
|
<div class="mb-2 text-sm">
|
||||||
<strong>Active</strong><br/>
|
<strong>Active</strong><br/>
|
||||||
<label><input type="checkbox" class="filter-active" value="Yes"> Yes</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-active" value="Yes"> Yes</label>
|
||||||
<label><input type="checkbox" class="filter-active" value="No"> No</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-active" value="No"> No</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-group">
|
<div class="mb-2 text-sm">
|
||||||
<strong>Estimator</strong><br/>
|
<strong>Estimator</strong><br/>
|
||||||
<label><input type="checkbox" class="filter-estimator" value="Steve Brewer"> Steve</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-estimator" value="Steve Brewer"> Steve</label>
|
||||||
<label><input type="checkbox" class="filter-estimator" value="Beth Cardoza"> Beth</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-estimator" value="Beth Cardoza"> Beth</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="filter-group">
|
<div class="mb-2 text-sm">
|
||||||
<strong>Status</strong><br/>
|
<strong>Status</strong><br/>
|
||||||
<label><input type="checkbox" class="filter-status" value="Estimating"> Estimating</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Estimating"> Estimating</label>
|
||||||
<label><input type="checkbox" class="filter-status" value="In Progress"> In Progress</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="In Progress"> In Progress</label>
|
||||||
<label><input type="checkbox" class="filter-status" value="Awarded"> Awarded</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Awarded"> Awarded</label>
|
||||||
<label><input type="checkbox" class="filter-status" value="Billed (In Progress)"> Billed (In Progress)</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Billed (In Progress)"> Billed (In Progress)</label>
|
||||||
<label><input type="checkbox" class="filter-status" value="Not Bidding"> Not Bidding</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Not Bidding"> Not Bidding</label>
|
||||||
<label><input type="checkbox" class="filter-status" value="Est Sent"> Est Sent</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Est Sent"> Est Sent</label>
|
||||||
<label><input type="checkbox" class="filter-status" value="Not Awarded"> Not Awarded</label>
|
<label class="mr-2 cursor-pointer text-gray-500"><input type="checkbox" class="filter-status" value="Not Awarded"> Not Awarded</label>
|
||||||
</div>
|
</div>
|
||||||
<button id="clearFilters" style="background:#6b7280;color:white;padding:8px;border-radius:8px;border:none;">Clear Filters</button>
|
<button id="clearFilters" class="bg-gray-500 text-white py-2 px-2 rounded-lg border-none">Clear Filters</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="results" aria-live="polite"></div>
|
<div id="results" aria-live="polite" class="flex flex-col gap-3 flex-1 overflow-auto p-0"></div>
|
||||||
<div class="footer">v1.0.0-beta2</div>
|
<div class="text-right text-xs text-gray-500 mt-5 pt-3 border-t border-gray-200">v1.0.0-beta2</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Detail Modal -->
|
<!-- Detail Modal -->
|
||||||
<div id="detailModal" role="dialog" aria-modal="true">
|
<div id="detailModal" role="dialog" aria-modal="true" class="hidden flex fixed inset-0 bg-black/45 items-start justify-center pt-[2vh] z-[999]">
|
||||||
<div id="detailBox">
|
<div id="detailBox" class="m-0 bg-white rounded-lg max-w-[380px] w-[92%] h-[90vh] max-h-[926px] overflow-hidden flex flex-col">
|
||||||
<h2 style="margin-top:0;color:var(--accent)">Job Detail</h2>
|
<!-- INFO Tab Content -->
|
||||||
|
<div id="infoContent" class="flex-1 overflow-hidden flex flex-col min-h-0">
|
||||||
<table class="detail-table" id="detailTable"></table>
|
<div id="infoHeading" class="bg-blue-600 text-white text-center py-2 text-sm font-semibold rounded-t-lg flex-shrink-0">INFO</div>
|
||||||
|
<div class="px-3 pt-2 pb-1 flex-1 overflow-auto">
|
||||||
<button id="jobFolderBtn" disabled>No Folder Available</button>
|
<div class="mt-0 w-full mb-1 space-y-2" id="detailTable"></div>
|
||||||
|
<button id="jobFolderBtn" disabled class="w-full py-2.5 px-3 rounded-lg border-none text-white cursor-pointer text-[17px] font-bold mb-2.5 transition-colors duration-200 bg-gray-600 cursor-not-allowed opacity-70">No Folder Available</button>
|
||||||
<h3 style="margin:8px 0 6px 0;color:var(--accent)">Notes</h3>
|
</div>
|
||||||
<div id="jobNotes"></div>
|
|
||||||
|
|
||||||
<div class="toolbar" id="toolbar">
|
|
||||||
<button data-cmd="bold" id="tbBold"><strong>B</strong></button>
|
|
||||||
<button data-cmd="italic" id="tbItalic"><em>I</em></button>
|
|
||||||
<button data-cmd="underline" id="tbUnderline"><u>U</u></button>
|
|
||||||
<button data-cmd="foreColor" data-val="#ff0000" id="tbRed" style="color:#ef4444">A</button>
|
|
||||||
<button data-cmd="foreColor" data-val="#0000ff" id="tbBlue" style="color:#2563eb">A</button>
|
|
||||||
<button data-cmd="foreColor" data-val="#000000" id="tbBlack" style="color:#000">A</button>
|
|
||||||
<button data-cmd="removeFormat" id="tbClear">Reset</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="newNote" contenteditable="true" aria-label="New note"></div>
|
<!-- NOTES Tab Content -->
|
||||||
|
<div id="notesContent" class="hidden flex-1 overflow-hidden flex flex-col min-h-0">
|
||||||
|
<div id="notesHeading" class="bg-orange-600 text-white text-center py-2 text-sm font-semibold rounded-t-lg flex-shrink-0">NOTES</div>
|
||||||
|
<div class="px-3 pt-2 pb-1 flex-1 overflow-auto">
|
||||||
|
<div id="jobNotes" class="min-h-[120px] max-h-[200px] overflow-auto p-1 border border-gray-600 rounded text-[11px] bg-transparent whitespace-pre-wrap break-words text-gray-600"></div>
|
||||||
|
|
||||||
<div class="modal-actions">
|
<div class="flex flex-wrap mb-0.5 mt-1.5" id="toolbar">
|
||||||
<button class="btn green" id="btnSubmit">Submit</button>
|
<button data-cmd="bold" id="tbBold" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px]"><strong>B</strong></button>
|
||||||
<button class="btn gray" id="btnClearNote">Clear</button>
|
<button data-cmd="italic" id="tbItalic" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px]"><em>I</em></button>
|
||||||
<button class="btn red" id="btnClose">Close</button>
|
<button data-cmd="underline" id="tbUnderline" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px]"><u>U</u></button>
|
||||||
|
<button data-cmd="foreColor" data-val="#ff0000" id="tbRed" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px] text-red-500">A</button>
|
||||||
|
<button data-cmd="foreColor" data-val="#0000ff" id="tbBlue" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px] text-blue-600">A</button>
|
||||||
|
<button data-cmd="foreColor" data-val="#000000" id="tbBlack" class="h-7 w-8 text-xs py-0 px-1 rounded border border-gray-400 bg-gray-400 text-gray-900 cursor-pointer flex items-center justify-center m-[1px] text-black">A</button>
|
||||||
|
<button data-cmd="removeFormat" id="tbClear" class="h-7 w-auto py-1 px-2 text-xs rounded border border-gray-400 bg-gray-400 text-white cursor-pointer flex items-center justify-center m-[1px]">Reset</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="newNote" contenteditable="true" aria-label="New note" class="min-h-14 max-h-[120px] overflow-auto p-2 border border-gray-600 rounded-lg text-base bg-transparent mt-1.5"></div>
|
||||||
|
|
||||||
|
<div class="flex gap-2 mt-1.5 mb-4">
|
||||||
|
<button class="py-2 px-2.5 rounded-lg border-none text-white cursor-pointer bg-green-600" id="btnSubmit">Submit</button>
|
||||||
|
<button class="py-2 px-2.5 rounded-lg border-none text-white cursor-pointer bg-gray-500" id="btnClearNote">Clear</button>
|
||||||
|
<button class="py-2 px-2.5 rounded-lg border-none text-white cursor-pointer bg-red-600" id="btnClose">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tab Bar at Bottom -->
|
||||||
|
<div class="flex items-center border-t border-gray-200 pt-2 px-3 pb-2 flex-shrink-0 bg-white rounded-b-lg">
|
||||||
|
<button id="backBtn" class="mr-6 pr-6 border-r border-gray-200 px-3 py-2 text-sm font-medium text-gray-600 hover:text-gray-800 flex items-center">
|
||||||
|
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
|
||||||
|
</svg>
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
<button id="infoTab" class="px-4 py-2 text-sm font-medium text-blue-600 border-b-2 border-blue-600 -mb-px">INFO</button>
|
||||||
|
<button id="notesTab" class="px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700">NOTES</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -215,11 +152,11 @@
|
|||||||
if(simulatedProg<65){ simulatedProg+=0.8; progressBar.style.width=Math.min(65,simulatedProg).toFixed(1)+'%'; }
|
if(simulatedProg<65){ simulatedProg+=0.8; progressBar.style.width=Math.min(65,simulatedProg).toFixed(1)+'%'; }
|
||||||
},150);
|
},150);
|
||||||
}
|
}
|
||||||
function finishProgress(){ if(progTimer) clearInterval(progTimer); progressBar.style.width='100%'; setTimeout(()=>{progressBar.parentElement.style.display='none';},400); }
|
function finishProgress(){ if(progTimer) clearInterval(progTimer); progressBar.style.width='100%'; setTimeout(()=>{progressBar.parentElement.classList.add('hidden');},400); }
|
||||||
|
|
||||||
// --- Fetch all jobs ---
|
// --- Fetch all jobs ---
|
||||||
async function fetchAllJobs(){
|
async function fetchAllJobs(){
|
||||||
startProgress(); jobsCache=[]; visibleJobs=[]; resultsEl.innerHTML = '<div style="padding:8px;color:#475569;background:#fff;border-radius:8px;border:1px solid #e6eefc">Loading…</div>';
|
startProgress(); jobsCache=[]; visibleJobs=[]; resultsEl.innerHTML = '<div class="p-2 text-slate-600 bg-white rounded-lg border border-blue-100">Loading…</div>';
|
||||||
let page=1, totalItems=0; let firstBatchRendered=false;
|
let page=1, totalItems=0; let firstBatchRendered=false;
|
||||||
try{
|
try{
|
||||||
while(true){
|
while(true){
|
||||||
@@ -251,13 +188,13 @@
|
|||||||
function renderInitialBatch(jobs){
|
function renderInitialBatch(jobs){
|
||||||
resultsEl.innerHTML='';
|
resultsEl.innerHTML='';
|
||||||
for(const job of jobs){
|
for(const job of jobs){
|
||||||
const card=document.createElement('div'); card.className='card';
|
const card=document.createElement('div'); card.className='bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1';
|
||||||
card.innerHTML=`
|
card.innerHTML=`
|
||||||
<div class="card-row"><div class="card-label">Job #:</div><div class="card-info">${escapeHtml(job.Job_Number||'')}</div></div>
|
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job #:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Number||'')}</div></div>
|
||||||
<div class="card-row"><div class="card-label">Job Name:</div><div class="card-info">${escapeHtml(job.Job_Full_Name||'')}</div></div>
|
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Full_Name||'')}</div></div>
|
||||||
<div class="card-row"><div class="card-label">Contact:</div><div class="card-info">${escapeHtml(job.Contact_Person||'')}</div></div>
|
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Contact_Person||'')}</div></div>
|
||||||
`;
|
`;
|
||||||
const dot=document.createElement('div'); dot.className='active-dot';
|
const dot=document.createElement('div'); dot.className='absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm';
|
||||||
dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1');
|
dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1');
|
||||||
card.appendChild(dot);
|
card.appendChild(dot);
|
||||||
card.addEventListener('click',()=>openDetail(job));
|
card.addEventListener('click',()=>openDetail(job));
|
||||||
@@ -310,19 +247,19 @@
|
|||||||
|
|
||||||
function renderCards(list){
|
function renderCards(list){
|
||||||
resultsEl.innerHTML='';
|
resultsEl.innerHTML='';
|
||||||
if(!list.length){ resultsEl.innerHTML = `<div style="padding:8px;color:#475569;background:#fff;border-radius:8px;border:1px solid #e6eefc">No results</div>`; return; }
|
if(!list.length){ resultsEl.innerHTML = `<div class="p-2 text-slate-600 bg-white rounded-lg border border-blue-100">No results</div>`; return; }
|
||||||
|
|
||||||
const firstChunk = list.slice(0,20);
|
const firstChunk = list.slice(0,20);
|
||||||
const remaining = list.slice(20);
|
const remaining = list.slice(20);
|
||||||
|
|
||||||
for(const job of firstChunk){
|
for(const job of firstChunk){
|
||||||
const card=document.createElement('div'); card.className='card';
|
const card=document.createElement('div'); card.className='bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1';
|
||||||
card.innerHTML=`
|
card.innerHTML=`
|
||||||
<div class="card-row"><div class="card-label">Job #:</div><div class="card-info">${escapeHtml(job.Job_Number||'')}</div></div>
|
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job #:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Number||'')}</div></div>
|
||||||
<div class="card-row"><div class="card-label">Job Name:</div><div class="card-info">${escapeHtml(job.Job_Full_Name||'')}</div></div>
|
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Full_Name||'')}</div></div>
|
||||||
<div class="card-row"><div class="card-label">Contact:</div><div class="card-info">${escapeHtml(job.Contact_Person||'')}</div></div>
|
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Contact_Person||'')}</div></div>
|
||||||
`;
|
`;
|
||||||
const dot=document.createElement('div'); dot.className='active-dot';
|
const dot=document.createElement('div'); dot.className='absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm';
|
||||||
dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1');
|
dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1');
|
||||||
card.appendChild(dot);
|
card.appendChild(dot);
|
||||||
card.addEventListener('click',()=>openDetail(job));
|
card.addEventListener('click',()=>openDetail(job));
|
||||||
@@ -332,13 +269,13 @@
|
|||||||
if(remaining.length){
|
if(remaining.length){
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
for(const job of remaining){
|
for(const job of remaining){
|
||||||
const card=document.createElement('div'); card.className='card';
|
const card=document.createElement('div'); card.className='bg-white rounded-lg p-2.5 border border-blue-100 shadow-sm cursor-pointer relative flex flex-col justify-center gap-1';
|
||||||
card.innerHTML=`
|
card.innerHTML=`
|
||||||
<div class="card-row"><div class="card-label">Job #:</div><div class="card-info">${escapeHtml(job.Job_Number||'')}</div></div>
|
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job #:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Number||'')}</div></div>
|
||||||
<div class="card-row"><div class="card-label">Job Name:</div><div class="card-info">${escapeHtml(job.Job_Full_Name||'')}</div></div>
|
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Job Name:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Job_Full_Name||'')}</div></div>
|
||||||
<div class="card-row"><div class="card-label">Contact:</div><div class="card-info">${escapeHtml(job.Contact_Person||'')}</div></div>
|
<div class="flex items-center gap-2 text-lg leading-tight mb-0.5"><div class="font-bold text-gray-700 min-w-[72px] whitespace-nowrap">Contact:</div><div class="text-gray-900 overflow-hidden text-ellipsis whitespace-nowrap flex-1">${escapeHtml(job.Contact_Person||'')}</div></div>
|
||||||
`;
|
`;
|
||||||
const dot=document.createElement('div'); dot.className='active-dot';
|
const dot=document.createElement('div'); dot.className='absolute top-2.5 right-2.5 w-3.5 h-3.5 rounded-full border-2 border-white shadow-sm';
|
||||||
dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1');
|
dot.style.background=(String(job.Active||'').toLowerCase()==='yes')?'green':((String(job.Active||'').toLowerCase()==='no')?'red':'#cbd5e1');
|
||||||
card.appendChild(dot);
|
card.appendChild(dot);
|
||||||
card.addEventListener('click',()=>openDetail(job));
|
card.addEventListener('click',()=>openDetail(job));
|
||||||
@@ -350,7 +287,7 @@
|
|||||||
function escapeHtml(str){ return String(str).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>'); }
|
function escapeHtml(str){ return String(str).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>'); }
|
||||||
|
|
||||||
// --- Filters ---
|
// --- Filters ---
|
||||||
filterToggle.addEventListener('click',()=>{ filtersPanel.style.display = filtersPanel.style.display==='block'?'none':'block'; filterToggle.textContent = filtersPanel.style.display==='block'?'Hide Filters':'Show Filters'; });
|
filterToggle.addEventListener('click',()=>{ filtersPanel.classList.toggle('hidden'); filterToggle.textContent = filtersPanel.classList.contains('hidden')?'Show Filters':'Hide Filters'; });
|
||||||
searchBox.addEventListener('input',()=>applyFiltersAndRender());
|
searchBox.addEventListener('input',()=>applyFiltersAndRender());
|
||||||
document.querySelectorAll('.filter-division').forEach(cb=>cb.addEventListener('change',()=>applyFiltersAndRender()));
|
document.querySelectorAll('.filter-division').forEach(cb=>cb.addEventListener('change',()=>applyFiltersAndRender()));
|
||||||
document.querySelectorAll('.filter-active').forEach(cb=>{ cb.addEventListener('change',()=>{
|
document.querySelectorAll('.filter-active').forEach(cb=>{ cb.addEventListener('change',()=>{
|
||||||
@@ -370,11 +307,121 @@
|
|||||||
currentJob = job;
|
currentJob = job;
|
||||||
detailTable.innerHTML = '';
|
detailTable.innerHTML = '';
|
||||||
const fields = ["Job_Full_Name","Job_Number","Company_Client","Manager","Contact_Person","Phone_Number","Job_Address","Start_Date","Tax_Exempt","Job_Status"];
|
const fields = ["Job_Full_Name","Job_Number","Company_Client","Manager","Contact_Person","Phone_Number","Job_Address","Start_Date","Tax_Exempt","Job_Status"];
|
||||||
for(const f of fields){
|
let i = 0;
|
||||||
const tr = document.createElement('tr');
|
while(i < fields.length){
|
||||||
const tdLabel = document.createElement('td'); tdLabel.className='label'; tdLabel.textContent=f.replace(/_/g,' ');
|
const f = fields[i];
|
||||||
const tdVal = document.createElement('td'); tdVal.textContent=job[f]||'';
|
|
||||||
tr.appendChild(tdLabel); tr.appendChild(tdVal); detailTable.appendChild(tr);
|
// Handle paired fields
|
||||||
|
if(f === 'Job_Number'){
|
||||||
|
const row = document.createElement('div'); row.className='pb-2 border-b border-gray-100 flex gap-4';
|
||||||
|
|
||||||
|
// Job Number
|
||||||
|
const col1 = document.createElement('div'); col1.className='flex-1';
|
||||||
|
const label1 = document.createElement('div'); label1.className='text-xs font-bold text-gray-500 uppercase tracking-wide mb-1'; label1.textContent='Job Number';
|
||||||
|
const val1 = document.createElement('div'); val1.className='text-sm text-gray-900'; val1.textContent = job['Job_Number'] || '—';
|
||||||
|
col1.appendChild(label1); col1.appendChild(val1);
|
||||||
|
|
||||||
|
// Company Client
|
||||||
|
const col2 = document.createElement('div'); col2.className='flex-1';
|
||||||
|
const label2 = document.createElement('div'); label2.className='text-xs font-bold text-gray-500 uppercase tracking-wide mb-1'; label2.textContent='Company Client';
|
||||||
|
const val2 = document.createElement('div'); val2.className='text-sm text-gray-900'; val2.textContent = job['Company_Client'] || '—';
|
||||||
|
col2.appendChild(label2); col2.appendChild(val2);
|
||||||
|
|
||||||
|
row.appendChild(col1); row.appendChild(col2); detailTable.appendChild(row);
|
||||||
|
i += 2; // Skip Company_Client in next iteration
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(f === 'Contact_Person'){
|
||||||
|
const row = document.createElement('div'); row.className='pb-2 border-b border-gray-100 flex gap-4';
|
||||||
|
|
||||||
|
// Contact Person
|
||||||
|
const col1 = document.createElement('div'); col1.className='flex-1';
|
||||||
|
const label1 = document.createElement('div'); label1.className='text-xs font-bold text-gray-500 uppercase tracking-wide mb-1'; label1.textContent='Contact Person';
|
||||||
|
const val1 = document.createElement('div'); val1.className='text-sm text-gray-900'; val1.textContent = job['Contact_Person'] || '—';
|
||||||
|
col1.appendChild(label1); col1.appendChild(val1);
|
||||||
|
|
||||||
|
// Phone Number
|
||||||
|
const col2 = document.createElement('div'); col2.className='flex-1';
|
||||||
|
const label2 = document.createElement('div'); label2.className='text-xs font-bold text-gray-500 uppercase tracking-wide mb-1'; label2.textContent='Phone Number';
|
||||||
|
const val2 = document.createElement('div'); val2.className='text-sm';
|
||||||
|
if(job['Phone_Number'] && job['Phone_Number'].trim() !== ''){
|
||||||
|
const phoneLink = document.createElement('a');
|
||||||
|
const phoneNumber = job['Phone_Number'].replace(/\D/g, ''); // Remove non-digits
|
||||||
|
phoneLink.href = `tel:${phoneNumber}`;
|
||||||
|
phoneLink.className = 'text-blue-600 hover:text-blue-800 underline';
|
||||||
|
phoneLink.textContent = job['Phone_Number'];
|
||||||
|
val2.appendChild(phoneLink);
|
||||||
|
} else {
|
||||||
|
val2.className = 'text-sm text-gray-900';
|
||||||
|
val2.textContent = '—';
|
||||||
|
}
|
||||||
|
col2.appendChild(label2); col2.appendChild(val2);
|
||||||
|
|
||||||
|
row.appendChild(col1); row.appendChild(col2); detailTable.appendChild(row);
|
||||||
|
i += 2; // Skip Phone_Number in next iteration
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regular single field
|
||||||
|
const value = job[f] || '';
|
||||||
|
if(!value && f === 'Tax_Exempt') {
|
||||||
|
i++;
|
||||||
|
continue; // Skip empty Tax Exempt
|
||||||
|
}
|
||||||
|
|
||||||
|
const row = document.createElement('div'); row.className='pb-2 border-b border-gray-100 last:border-0';
|
||||||
|
const label = document.createElement('div'); label.className='text-xs font-bold text-gray-500 uppercase tracking-wide mb-1'; label.textContent=f.replace(/_/g,' ');
|
||||||
|
const val = document.createElement('div'); val.className='text-sm text-gray-900';
|
||||||
|
|
||||||
|
// Format date if it's Start_Date
|
||||||
|
if(f === 'Start_Date' && value){
|
||||||
|
try {
|
||||||
|
const date = new Date(value);
|
||||||
|
val.textContent = date.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
|
||||||
|
} catch(e) {
|
||||||
|
val.textContent = value;
|
||||||
|
}
|
||||||
|
} else if(f === 'Job_Address' && value && value.trim() !== ''){
|
||||||
|
// Create address container with text and map icons
|
||||||
|
val.className = 'text-sm flex items-center gap-2 flex-wrap';
|
||||||
|
|
||||||
|
// Address text
|
||||||
|
const addressText = document.createElement('span');
|
||||||
|
addressText.className = 'text-gray-900 flex-1 min-w-0';
|
||||||
|
addressText.textContent = value;
|
||||||
|
val.appendChild(addressText);
|
||||||
|
|
||||||
|
// Icon container
|
||||||
|
const iconContainer = document.createElement('div');
|
||||||
|
iconContainer.className = 'flex items-center gap-2';
|
||||||
|
|
||||||
|
// Apple Maps link
|
||||||
|
const appleLink = document.createElement('a');
|
||||||
|
const encodedAddress = encodeURIComponent(value);
|
||||||
|
appleLink.href = `https://maps.apple.com/?q=${encodedAddress}`;
|
||||||
|
appleLink.target = '_blank';
|
||||||
|
appleLink.className = 'inline-flex items-center justify-center w-6 h-6 hover:opacity-80 transition-opacity';
|
||||||
|
appleLink.title = 'Open in Apple Maps';
|
||||||
|
appleLink.innerHTML = '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" style="fill: #000000;"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>';
|
||||||
|
iconContainer.appendChild(appleLink);
|
||||||
|
|
||||||
|
// Google Maps link
|
||||||
|
const googleLink = document.createElement('a');
|
||||||
|
googleLink.href = `https://www.google.com/maps/search/?api=1&query=${encodedAddress}`;
|
||||||
|
googleLink.target = '_blank';
|
||||||
|
googleLink.className = 'inline-flex items-center justify-center w-6 h-6 hover:opacity-80 transition-opacity';
|
||||||
|
googleLink.title = 'Open in Google Maps';
|
||||||
|
googleLink.innerHTML = '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" style="fill: #4285F4;"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>';
|
||||||
|
iconContainer.appendChild(googleLink);
|
||||||
|
|
||||||
|
val.appendChild(iconContainer);
|
||||||
|
} else {
|
||||||
|
val.textContent = value || '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
row.appendChild(label); row.appendChild(val); detailTable.appendChild(row);
|
||||||
|
i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Job folder button
|
// Job folder button
|
||||||
@@ -383,28 +430,61 @@
|
|||||||
const url=new URL(job.Job_Folder_Link);
|
const url=new URL(job.Job_Folder_Link);
|
||||||
jobFolderBtn.textContent='Show Job Folder';
|
jobFolderBtn.textContent='Show Job Folder';
|
||||||
jobFolderBtn.disabled=false;
|
jobFolderBtn.disabled=false;
|
||||||
jobFolderBtn.className='active';
|
jobFolderBtn.className='w-full py-2.5 px-3 rounded-lg border-none text-white cursor-pointer text-[17px] font-bold mb-2.5 transition-colors duration-200 bg-blue-600 hover:bg-blue-700';
|
||||||
jobFolderBtn.onclick=()=>window.open(url.href, '_blank', 'noopener,noreferrer');
|
jobFolderBtn.onclick=()=>window.open(url.href, '_blank', 'noopener,noreferrer');
|
||||||
}catch(e){
|
}catch(e){
|
||||||
jobFolderBtn.textContent='No Folder Available';
|
jobFolderBtn.textContent='No Folder Available';
|
||||||
jobFolderBtn.disabled=true;
|
jobFolderBtn.disabled=true;
|
||||||
jobFolderBtn.className='';
|
jobFolderBtn.className='w-full py-2.5 px-3 rounded-lg border-none text-white cursor-pointer text-[17px] font-bold mb-2.5 transition-colors duration-200 bg-gray-600 cursor-not-allowed opacity-70';
|
||||||
jobFolderBtn.onclick=null;
|
jobFolderBtn.onclick=null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
jobFolderBtn.textContent='No Folder Available';
|
jobFolderBtn.textContent='No Folder Available';
|
||||||
jobFolderBtn.disabled=true;
|
jobFolderBtn.disabled=true;
|
||||||
jobFolderBtn.className='';
|
jobFolderBtn.className='w-full py-2.5 px-3 rounded-lg border-none text-white cursor-pointer text-[17px] font-bold mb-2.5 transition-colors duration-200 bg-gray-600 cursor-not-allowed opacity-70';
|
||||||
jobFolderBtn.onclick=null;
|
jobFolderBtn.onclick=null;
|
||||||
}
|
}
|
||||||
|
|
||||||
jobNotes.innerHTML = job.Notes || '';
|
jobNotes.innerHTML = job.Notes || '';
|
||||||
newNote.innerHTML = '';
|
newNote.innerHTML = '';
|
||||||
tbButtons.forEach(b=>b.classList.remove('active'));
|
tbButtons.forEach(b=>b.classList.remove('bg-blue-100', 'text-gray-900', 'border-blue-600'));
|
||||||
detailModal.style.display='flex';
|
detailModal.classList.remove('hidden');
|
||||||
document.getElementById('detailBox').scrollTop = 0;
|
document.getElementById('detailBox').scrollTop = 0;
|
||||||
|
|
||||||
|
// Reset to INFO tab when opening
|
||||||
|
switchTab('info');
|
||||||
}
|
}
|
||||||
document.getElementById('btnClose').addEventListener('click',()=>{detailModal.style.display='none';});
|
document.getElementById('btnClose').addEventListener('click',()=>{detailModal.classList.add('hidden');});
|
||||||
|
document.getElementById('backBtn').addEventListener('click',()=>{detailModal.classList.add('hidden');});
|
||||||
|
|
||||||
|
// Tab switching
|
||||||
|
function switchTab(tab){
|
||||||
|
const infoTab = document.getElementById('infoTab');
|
||||||
|
const notesTab = document.getElementById('notesTab');
|
||||||
|
const infoContent = document.getElementById('infoContent');
|
||||||
|
const notesContent = document.getElementById('notesContent');
|
||||||
|
const infoHeading = document.getElementById('infoHeading');
|
||||||
|
const notesHeading = document.getElementById('notesHeading');
|
||||||
|
|
||||||
|
if(tab === 'info'){
|
||||||
|
infoTab.className = 'px-4 py-2 text-sm font-medium text-blue-600 border-b-2 border-blue-600 -mb-px';
|
||||||
|
notesTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700';
|
||||||
|
infoContent.classList.remove('hidden');
|
||||||
|
notesContent.classList.add('hidden');
|
||||||
|
if(infoHeading) infoHeading.classList.remove('hidden');
|
||||||
|
if(notesHeading) notesHeading.classList.add('hidden');
|
||||||
|
} else {
|
||||||
|
infoTab.className = 'px-4 py-2 text-sm font-medium text-gray-500 hover:text-gray-700';
|
||||||
|
notesTab.className = 'px-4 py-2 text-sm font-medium text-orange-600 border-b-2 border-orange-600 -mb-px';
|
||||||
|
infoContent.classList.add('hidden');
|
||||||
|
notesContent.classList.remove('hidden');
|
||||||
|
if(infoHeading) infoHeading.classList.add('hidden');
|
||||||
|
if(notesHeading) notesHeading.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('infoTab').addEventListener('click',()=>switchTab('info'));
|
||||||
|
document.getElementById('notesTab').addEventListener('click',()=>switchTab('notes'));
|
||||||
|
|
||||||
// --- Toolbar ---
|
// --- Toolbar ---
|
||||||
let savedSelection = null;
|
let savedSelection = null;
|
||||||
@@ -417,9 +497,9 @@
|
|||||||
restoreSelection();
|
restoreSelection();
|
||||||
const cmd = btn.dataset.cmd;
|
const cmd = btn.dataset.cmd;
|
||||||
if(!cmd) return;
|
if(!cmd) return;
|
||||||
if(cmd==='foreColor'){ const val=btn.dataset.val; document.execCommand(cmd,false,val); tbButtons.forEach(b=>{if(b.dataset.cmd==='foreColor'&&b!==btn)b.classList.remove('active');}); btn.classList.toggle('active'); }
|
if(cmd==='foreColor'){ const val=btn.dataset.val; document.execCommand(cmd,false,val); tbButtons.forEach(b=>{if(b.dataset.cmd==='foreColor'&&b!==btn)b.classList.remove('bg-blue-100', 'text-gray-900', 'border-blue-600');}); btn.classList.toggle('bg-blue-100'); btn.classList.toggle('text-gray-900'); btn.classList.toggle('border-blue-600'); }
|
||||||
else if(cmd==='removeFormat'){ restoreSelection(); const sel=window.getSelection(); if(sel.rangeCount>0){ const range=sel.getRangeAt(0); let content=range.toString(); range.deleteContents(); range.insertNode(document.createTextNode(content)); range.setStartAfter(range.startContainer); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } tbButtons.forEach(b=>b.classList.remove('active')); }
|
else if(cmd==='removeFormat'){ restoreSelection(); const sel=window.getSelection(); if(sel.rangeCount>0){ const range=sel.getRangeAt(0); let content=range.toString(); range.deleteContents(); range.insertNode(document.createTextNode(content)); range.setStartAfter(range.startContainer); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } tbButtons.forEach(b=>b.classList.remove('bg-blue-100', 'text-gray-900', 'border-blue-600')); }
|
||||||
else{ document.execCommand(cmd,false,null); btn.classList.toggle('active'); }
|
else{ document.execCommand(cmd,false,null); btn.classList.toggle('bg-blue-100'); btn.classList.toggle('text-gray-900'); btn.classList.toggle('border-blue-600'); }
|
||||||
newNote.focus(); saveSelection();
|
newNote.focus(); saveSelection();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+21
-138
@@ -4,143 +4,26 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Sign In</title>
|
<title>Sign In</title>
|
||||||
|
<script src="https://cdn.tailwindcss.com"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||||
<script src="auth.js"></script>
|
<script src="auth.js"></script>
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
min-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
background: white;
|
|
||||||
padding: 3rem;
|
|
||||||
border-radius: 1rem;
|
|
||||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
|
||||||
text-align: center;
|
|
||||||
max-width: 400px;
|
|
||||||
width: 90%;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #333;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
font-size: 1.75rem;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
}
|
|
||||||
button {
|
|
||||||
background: #0078d4;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
padding: 0.875rem 2rem;
|
|
||||||
font-size: 1rem;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
button:hover:not(:disabled) {
|
|
||||||
background: #005a9e;
|
|
||||||
}
|
|
||||||
button:disabled {
|
|
||||||
background: #ccc;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.user-info {
|
|
||||||
display: none;
|
|
||||||
margin-top: 2rem;
|
|
||||||
text-align: left;
|
|
||||||
background: #f8f9fa;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
.user-info h2 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.user-info p {
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
color: #555;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
.user-info strong {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.error {
|
|
||||||
color: #d32f2f;
|
|
||||||
margin-top: 1rem;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.logout-btn {
|
|
||||||
background: #6c757d;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
.logout-btn:hover {
|
|
||||||
background: #5a6268;
|
|
||||||
}
|
|
||||||
.notes-section {
|
|
||||||
display: none;
|
|
||||||
margin-top: 2rem;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.notes-section h3 {
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
textarea {
|
|
||||||
width: 100%;
|
|
||||||
padding: 0.75rem;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
font-family: inherit;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
resize: vertical;
|
|
||||||
min-height: 80px;
|
|
||||||
}
|
|
||||||
textarea:focus {
|
|
||||||
outline: none;
|
|
||||||
border-color: #0078d4;
|
|
||||||
}
|
|
||||||
.submit-btn {
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.success {
|
|
||||||
color: #2e7d32;
|
|
||||||
margin-top: 0.75rem;
|
|
||||||
display: none;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="font-sans bg-gradient-to-br from-indigo-500 to-purple-600 min-h-screen flex items-center justify-center">
|
||||||
<div class="container">
|
<div class="bg-white p-12 rounded-2xl shadow-2xl text-center max-w-md w-[90%]">
|
||||||
<h1>Welcome</h1>
|
<h1 class="text-gray-800 mb-2 text-3xl">Welcome</h1>
|
||||||
<p>Sign in with your Microsoft account</p>
|
<p class="text-gray-600 mb-8">Sign in with your Microsoft account</p>
|
||||||
|
|
||||||
<button id="loginBtn" onclick="login()">Sign in with Microsoft</button>
|
<button id="loginBtn" onclick="login()" class="bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed">Sign in with Microsoft</button>
|
||||||
|
|
||||||
<div class="error" id="error"></div>
|
<div class="text-red-600 mt-4 hidden" id="error"></div>
|
||||||
|
|
||||||
<div class="user-info" id="userInfo">
|
<div class="hidden mt-8 text-left bg-gray-50 p-6 rounded-lg" id="userInfo">
|
||||||
<h2>Signed in</h2>
|
<h2 class="text-lg mb-4 text-gray-800">Signed in</h2>
|
||||||
<p><strong>Name:</strong> <span id="displayName"></span></p>
|
<p class="mb-3 text-gray-600 break-all"><strong class="text-gray-800">Name:</strong> <span id="displayName"></span></p>
|
||||||
<p><strong>Email:</strong> <span id="email"></span></p>
|
<p class="mb-3 text-gray-600 break-all"><strong class="text-gray-800">Email:</strong> <span id="email"></span></p>
|
||||||
|
|
||||||
<button id="continueBtn" style="display:none;margin-top:1rem;" onclick="goToIndex()">Continue to Job Info</button>
|
<button id="continueBtn" onclick="goToIndex()" class="hidden mt-4 bg-blue-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed w-full">Continue to Job Info</button>
|
||||||
<button class="logout-btn" onclick="logout()">Sign out</button>
|
<button onclick="logout()" class="bg-gray-600 text-white border-none py-3.5 px-8 text-base rounded-md cursor-pointer transition-colors duration-200 font-medium hover:bg-gray-700 mt-4">Sign out</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -157,11 +40,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showAuthedUI(displayName, email) {
|
function showAuthedUI(displayName, email) {
|
||||||
loginBtn.style.display = 'none';
|
loginBtn.classList.add('hidden');
|
||||||
loginBtn.disabled = false;
|
loginBtn.disabled = false;
|
||||||
loginBtn.textContent = 'Sign in with Microsoft';
|
loginBtn.textContent = 'Sign in with Microsoft';
|
||||||
userInfo.style.display = 'block';
|
userInfo.classList.remove('hidden');
|
||||||
continueBtn.style.display = 'block';
|
continueBtn.classList.remove('hidden');
|
||||||
document.getElementById('displayName').textContent = displayName || 'Unknown';
|
document.getElementById('displayName').textContent = displayName || 'Unknown';
|
||||||
document.getElementById('email').textContent = email || 'Not available';
|
document.getElementById('email').textContent = email || 'Not available';
|
||||||
}
|
}
|
||||||
@@ -169,7 +52,7 @@
|
|||||||
async function login() {
|
async function login() {
|
||||||
loginBtn.disabled = true;
|
loginBtn.disabled = true;
|
||||||
loginBtn.textContent = 'Signing in...';
|
loginBtn.textContent = 'Signing in...';
|
||||||
errorEl.style.display = 'none';
|
errorEl.classList.add('hidden');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authData = await pb.collection('users').authWithOAuth2({
|
const authData = await pb.collection('users').authWithOAuth2({
|
||||||
@@ -179,7 +62,7 @@
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login failed:', error);
|
console.error('Login failed:', error);
|
||||||
errorEl.textContent = error.message || 'Authentication failed. Please try again.';
|
errorEl.textContent = error.message || 'Authentication failed. Please try again.';
|
||||||
errorEl.style.display = 'block';
|
errorEl.classList.remove('hidden');
|
||||||
loginBtn.disabled = false;
|
loginBtn.disabled = false;
|
||||||
loginBtn.textContent = 'Sign in with Microsoft';
|
loginBtn.textContent = 'Sign in with Microsoft';
|
||||||
}
|
}
|
||||||
@@ -187,9 +70,9 @@
|
|||||||
|
|
||||||
function logout() {
|
function logout() {
|
||||||
pb.authStore.clear();
|
pb.authStore.clear();
|
||||||
loginBtn.style.display = 'block';
|
loginBtn.classList.remove('hidden');
|
||||||
userInfo.style.display = 'none';
|
userInfo.classList.add('hidden');
|
||||||
continueBtn.style.display = 'none';
|
continueBtn.classList.add('hidden');
|
||||||
loginBtn.disabled = false;
|
loginBtn.disabled = false;
|
||||||
loginBtn.textContent = 'Sign in with Microsoft';
|
loginBtn.textContent = 'Sign in with Microsoft';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>
|
||||||
|
After Width: | Height: | Size: 650 B |
@@ -0,0 +1 @@
|
|||||||
|
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>
|
||||||
|
After Width: | Height: | Size: 457 B |
Reference in New Issue
Block a user