diff --git a/portal/templates/app/request.html b/portal/templates/app/request.html index 3938a67..d6c6d11 100644 --- a/portal/templates/app/request.html +++ b/portal/templates/app/request.html @@ -41,38 +41,60 @@ Admin -
- -
-
-
-

Loading...

-

+
+ + +
+ + +
+
+ ๐Ÿ“‹ +

Request

+
+
+

Loading...

+ +
+

+
+ + +
-
-
- - + + +
+
+ ๐Ÿ“ +

Response

+
+ +
+
+
๐Ÿ“Ž
+

Drop files to submit response

+

PDF, DOCX, XLSX, images

+ +
+
+
- -
-

Response

-
-
-
๐Ÿ“Ž
-

Drop files here or click to upload

-

PDF, DOCX, XLSX, images

- -
-
-
- - +
-

Comments

+
+ ๐Ÿ’ฌ +

Discussion

+
@@ -100,93 +122,178 @@ function escHtml(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } const statusColors = { open: 'bg-yellow-500/20 text-yellow-300', answered: 'bg-green-500/20 text-green-300', closed: 'bg-gray-500/20 text-gray-300', 'under-review': 'bg-blue-500/20 text-blue-300' }; + const priorityColors = { high: 'bg-red-500/20 text-red-300', medium: 'bg-yellow-500/20 text-yellow-300', low: 'bg-blue-500/20 text-blue-300' }; - async function loadRequest() { + let currentRequest = null; // stores the full request entry + let currentData = null; // stores parsed data_text + + async function loadAll() { try { - const res = await fetchAPI('/api/entries/' + reqID); + const res = await fetchAPI('/api/requests/' + reqID); if (!res.ok) { document.getElementById('reqTitle').textContent = 'Not found'; return; } - const req = await res.json(); - const d = parseData(req.data_text); - const title = d.title || req.summary || 'Untitled'; - document.title = title + ' โ€” Dealspace'; - document.getElementById('reqRef').textContent = d.ref || title; - document.getElementById('reqTitle').textContent = title; - document.getElementById('reqDesc').textContent = d.description || ''; - if (req.project_id) document.getElementById('backToProject').href = '/app/projects/' + req.project_id; - const status = d.status || 'open'; - const sc = statusColors[status] || 'bg-gray-500/20 text-gray-300'; - document.getElementById('reqStatus').className = 'shrink-0 px-3 py-1 rounded-full text-sm font-medium capitalize ' + sc; - document.getElementById('reqStatus').textContent = status; - if (d.due_date) document.getElementById('reqDue').textContent = '๐Ÿ“… Due: ' + d.due_date; - if (d.assignee) document.getElementById('reqAssignee').textContent = '๐Ÿ‘ค ' + d.assignee; - } catch(e) {} + const json = await res.json(); + currentRequest = json.request; + currentData = parseData(currentRequest.data_text); + const children = json.children || []; + + renderRequest(currentRequest, currentData); + renderAnswers(children.filter(c => c.type === 'answer' || c.type === 'document')); + renderComments(children.filter(c => c.type === 'comment')); + } catch(e) { + document.getElementById('reqTitle').textContent = 'Error loading request'; + } } - async function loadAnswers() { - try { - const res = await fetchAPI('/api/entries/' + reqID + '/children?type=answer'); - const items = await res.json(); - const el = document.getElementById('answers'); - if (!items || items.length === 0) { el.innerHTML = '

No documents uploaded yet.

'; return; } - el.innerHTML = items.map(a => { - const d = parseData(a.data_text); - const name = d.filename || d.name || a.summary || 'Document'; - return `
- ${name.endsWith('.pdf') ? '๐Ÿ“„' : name.match(/\.(jpg|jpeg|png|gif)$/i) ? '๐Ÿ–ผ๏ธ' : '๐Ÿ“Ž'} -
-
${escHtml(name)}
-
${new Date(a.created_at).toLocaleString()}
+ function renderRequest(req, d) { + const title = d.title || req.summary || 'Untitled'; + document.title = title + ' โ€” Dealspace'; + document.getElementById('reqRef').textContent = d.item_number || title; + document.getElementById('reqTitle').textContent = title; + document.getElementById('reqDesc').textContent = d.description || ''; + + if (req.project_id) document.getElementById('backToProject').href = '/app/projects/' + req.project_id; + + // Item number / section badge + const badge = document.getElementById('reqItemBadge'); + const badgeParts = [d.section, d.item_number].filter(Boolean); + if (badgeParts.length) { badge.textContent = badgeParts.join(' ยท '); badge.classList.remove('hidden'); } + + // Priority + const prioEl = document.getElementById('reqPriority'); + if (d.priority) { + const pc = priorityColors[d.priority] || 'bg-gray-500/20 text-gray-300'; + prioEl.className = 'px-2.5 py-1 rounded-full text-xs font-medium capitalize ' + pc; + prioEl.textContent = d.priority + ' priority'; + prioEl.classList.remove('hidden'); + } + + // Due date + if (d.due_date) { + const dueEl = document.getElementById('reqDue'); + dueEl.textContent = '๐Ÿ“… Due: ' + d.due_date; + dueEl.classList.remove('hidden'); + } + + // Status + const status = d.status || 'open'; + const sc = statusColors[status] || 'bg-gray-500/20 text-gray-300'; + const statusEl = document.getElementById('reqStatus'); + statusEl.className = 'px-2.5 py-1 rounded-full text-xs font-medium capitalize ' + sc; + statusEl.textContent = status; + + // Answered banner + mark-answered button + if (status === 'answered') { + document.getElementById('answeredBanner').classList.remove('hidden'); + document.getElementById('markAnsweredBtn').classList.add('hidden'); + } else { + document.getElementById('answeredBanner').classList.add('hidden'); + document.getElementById('markAnsweredBtn').classList.remove('hidden'); + } + } + + function renderAnswers(docs) { + const el = document.getElementById('answers'); + if (!docs || docs.length === 0) { + el.innerHTML = '

No response submitted yet.

'; + return; + } + const projectID = currentRequest.project_id; + el.innerHTML = docs.map(a => { + const d = parseData(a.data_text); + const name = d.filename || d.name || a.summary || 'Document'; + const icon = name.endsWith('.pdf') ? '๐Ÿ“„' : name.match(/\.(jpg|jpeg|png|gif)$/i) ? '๐Ÿ–ผ๏ธ' : name.match(/\.(xlsx?|csv)$/i) ? '๐Ÿ“Š' : '๐Ÿ“Ž'; + const objectID = d.object_id || ''; + const downloadUrl = objectID ? '/api/projects/' + projectID + '/objects/' + objectID + '?filename=' + encodeURIComponent(name) : '#'; + return `
+ ${icon} +
+
${escHtml(name)}
+
${a.created_at ? new Date(a.created_at * 1000).toLocaleString() : ''}
+
+ ${objectID ? `Download` : ''} +
`; + }).join(''); + } + + function renderComments(comments) { + const el = document.getElementById('comments'); + if (!comments || comments.length === 0) { + el.innerHTML = '

No comments yet.

'; + return; + } + el.innerHTML = comments.map(c => { + const d = parseData(c.data_text); + return `
+
${(d.author||'?')[0].toUpperCase()}
+
+
+ ${escHtml(d.author||'Unknown')} + ${c.created_at ? new Date(c.created_at * 1000).toLocaleString() : ''}
- Download -
`; - }).join(''); - } catch(e) {} +

${escHtml(d.text||'')}

+
+
`; + }).join(''); } async function uploadFiles(files) { + if (!currentRequest) return; + const projectID = currentRequest.project_id; const status = document.getElementById('uploadStatus'); for (const file of files) { status.textContent = 'Uploading ' + file.name + '...'; const fd = new FormData(); fd.append('file', file); - fd.append('parent_id', reqID); try { - const res = await fetchAPI('/api/upload', { method: 'POST', body: fd }); - if (res.ok) { status.textContent = file.name + ' uploaded.'; loadAnswers(); } - else { status.textContent = 'Upload failed for ' + file.name; } + // 1. Upload file to object store + const uploadRes = await fetchAPI('/api/projects/' + projectID + '/objects', { method: 'POST', body: fd }); + if (!uploadRes.ok) { status.textContent = 'Upload failed for ' + file.name; continue; } + const uploadData = await uploadRes.json(); + + // 2. Create document entry linked to request + const entryBody = { + project_id: projectID, + parent_id: reqID, + type: 'document', + data: JSON.stringify({ filename: file.name, name: file.name, content_type: file.type, object_id: uploadData.object_id }) + }; + const entryRes = await fetchAPI('/api/projects/' + projectID + '/entries', { method: 'POST', body: JSON.stringify(entryBody) }); + if (entryRes.ok) { status.textContent = file.name + ' uploaded.'; } + else { status.textContent = 'Failed to save ' + file.name; } } catch(e) { status.textContent = 'Error: ' + e.message; } } setTimeout(() => status.textContent = '', 3000); - } - - async function loadComments() { - try { - const res = await fetchAPI('/api/entries/' + reqID + '/children?type=comment'); - const items = await res.json(); - const el = document.getElementById('comments'); - if (!items || items.length === 0) { el.innerHTML = '

No comments yet.

'; return; } - el.innerHTML = items.map(c => { - const d = parseData(c.data_text); - return `
-
${(d.author||'?')[0].toUpperCase()}
-
-
- ${escHtml(d.author||'Unknown')} - ${new Date(c.created_at).toLocaleString()} -
-

${escHtml(d.text||'')}

-
-
`; - }).join(''); - } catch(e) {} + loadAll(); // refresh } async function postComment() { + if (!currentRequest) return; const text = document.getElementById('commentText').value.trim(); if (!text) return; + const projectID = currentRequest.project_id; + const body = { + project_id: projectID, + parent_id: reqID, + type: 'comment', + data: JSON.stringify({ text, author: user.name || user.email }) + }; try { - const res = await fetchAPI('/api/entries/' + reqID + '/children', { method: 'POST', body: JSON.stringify({ type: 'comment', data: { text, author: user.name || user.email } }) }); - if (res.ok) { document.getElementById('commentText').value = ''; loadComments(); } + const res = await fetchAPI('/api/projects/' + projectID + '/entries', { method: 'POST', body: JSON.stringify(body) }); + if (res.ok) { document.getElementById('commentText').value = ''; loadAll(); } + } catch(e) {} + } + + async function markAnswered() { + if (!currentRequest || !currentData) return; + const projectID = currentRequest.project_id; + const updatedData = { ...currentData, status: 'answered' }; + const body = { + data: JSON.stringify(updatedData), + version: currentRequest.version + }; + try { + const res = await fetchAPI('/api/projects/' + projectID + '/entries/' + reqID, { method: 'PUT', body: JSON.stringify(body) }); + if (res.ok) loadAll(); } catch(e) {} } @@ -196,9 +303,7 @@ ua.addEventListener('dragleave', () => ua.classList.remove('border-[#c9a84c]/60')); ua.addEventListener('drop', e => { e.preventDefault(); ua.classList.remove('border-[#c9a84c]/60'); uploadFiles(e.dataTransfer.files); }); - loadRequest(); - loadAnswers(); - loadComments(); + loadAll();