A Pen by Josh Miller on CodePen.
Created
September 20, 2024 13:42
-
-
Save jshmllr/747534dc0db4da44af0213636295f18d to your computer and use it in GitHub Desktop.
Co-Pro IV
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Co-Pro Settlement Table</title> | |
<link rel="stylesheet" href="styles.css"> | |
</head> | |
<body> | |
<div class="controls"> | |
<label for="costBasis">Reference</label> | |
<select id="costBasis"> | |
<option value="internal">Internal</option> | |
<option value="artist">External</option> | |
</select> | |
</div> | |
<table id="coProTable"> | |
<thead> | |
<tr> | |
<th>Item</th> | |
<th class="right-align">Prism Promoter</th> | |
<th class="right-align">Live Nation</th> | |
<th class="right-align disabled-column">Co-Pro Expenses</th> | |
<th class="right-align disabled-column">Internal Expenses</th> | |
</tr> | |
</thead> | |
<tbody> | |
<!-- Update all instances of data-label="Co-Pro Revenue" to data-label="Co-Pro Expenses" --> | |
<tr> | |
<td data-label="Item">Trey Kennedy</td> | |
<td class="right-align editable" data-label="Savelive" data-editable>$23,461.73</td> | |
<td class="right-align editable" data-label="Live Nation" data-editable>$0.00</td> | |
<td class="right-align revenue-diff disabled-column" data-label="Co-Pro Expenses"> | |
<div class="revenue-wrapper"> | |
<span class="revenue-amount">$23,461.73</span> | |
<sup class="revenue-difference">(+$0.00)</sup> | |
</div> | |
</td> | |
<td class="right-align disabled-column" data-label="Internal Expenses">$23,461.73</td> | |
</tr> | |
</tr> | |
<tr> | |
<td data-label="Item">Backline Rental (keyboard only)</td> | |
<td class="right-align editable" data-label="Savelive" data-editable>$127.50</td> | |
<td class="right-align editable" data-label="Live Nation" data-editable>$0.00</td> | |
<td class="right-align revenue-diff positive disabled-column" data-label="Co-Pro Revenue"> | |
<div class="revenue-wrapper"> | |
<span class="revenue-amount">$227.50</span> | |
<sup class="revenue-difference positive">(+$50.00)</sup> | |
</div> | |
</td> | |
<td class="right-align disabled-column" data-label="Internal Expenses">$177.50</td> | |
</tr> | |
<tr> | |
<td data-label="Item">Stage Manager</td> | |
<td class="right-align editable" data-label="Savelive" data-editable>$250.00</td> | |
<td class="right-align editable" data-label="Live Nation" data-editable>$0.00</td> | |
<td class="right-align revenue-diff negative disabled-column" data-label="Co-Pro Revenue"> | |
<div class="revenue-wrapper"> | |
<span class="revenue-amount">$450.00</span> | |
<sup class="revenue-difference negative">(-$50.00)</sup> | |
</div> | |
</td> | |
<td class="right-align disabled-column" data-label="Internal Expenses">$500.00</td> | |
</tr> | |
<tr id="totalsRow"> | |
<td data-label="Item"><strong>Totals</strong></td> | |
<td class="right-align" data-label="Savelive"></td> | |
<td class="right-align" data-label="Live Nation"></td> | |
<td class="right-align revenue-diff disabled-column" data-label="Co-Pro Revenue"> | |
<div class="revenue-wrapper"> | |
<span class="revenue-amount"></span> | |
<sup class="revenue-difference"></sup> | |
</div> | |
</td> | |
<td class="right-align disabled-column" data-label="Internal Expenses"></td> | |
</tr> | |
</tbody> | |
</table> | |
<script src="script.js"></script> | |
</body> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
document.addEventListener("DOMContentLoaded", (event) => { | |
console.log("DOM fully loaded and parsed"); | |
const editableCells = document.querySelectorAll("[data-editable]"); | |
const costBasisDropdown = document.getElementById("costBasis"); | |
const coProTable = document.getElementById("coProTable"); | |
const expensesColumnHeader = coProTable.querySelector("thead th:nth-child(5)"); | |
console.log(`Found ${editableCells.length} editable cells`); | |
// Mock data for artist settlement costs | |
const artistSettlementCosts = { | |
"Trey Kennedy": "$24,000.00", | |
"Backline Rental (keyboard only)": "$200.00", | |
"Stage Manager": "$550.00" | |
}; | |
// Store original internal costs | |
const originalInternalCosts = {}; | |
coProTable.querySelectorAll("tbody tr:not(#totalsRow)").forEach((row) => { | |
const itemName = row.cells[0].textContent; | |
originalInternalCosts[itemName] = row.cells[4].textContent; | |
}); | |
function updateTotals() { | |
const rows = coProTable.querySelectorAll("tbody tr:not(#totalsRow)"); | |
const totalsRow = document.getElementById("totalsRow"); | |
let totalSavelive = 0; | |
let totalLiveNation = 0; | |
let totalCoProExpenses = 0; | |
let totalInternalExpenses = 0; | |
rows.forEach((row) => { | |
totalSavelive += parseFloat(row.cells[1].textContent.replace(/[^\d.-]/g, "")); | |
totalLiveNation += parseFloat(row.cells[2].textContent.replace(/[^\d.-]/g, "")); | |
totalCoProExpenses += parseFloat(row.cells[3].querySelector(".revenue-amount").textContent.replace(/[^\d.-]/g, "")); | |
totalInternalExpenses += parseFloat(row.cells[4].textContent.replace(/[^\d.-]/g, "")); | |
}); | |
totalsRow.cells[1].innerHTML = `<strong>$${formatNumber(totalSavelive)}</strong>`; | |
totalsRow.cells[2].innerHTML = `<strong>$${formatNumber(totalLiveNation)}</strong>`; | |
const coProDifference = totalCoProExpenses - totalInternalExpenses; | |
let differenceHtml = ""; | |
if (Math.abs(coProDifference) > 0.01) { | |
differenceHtml = ` | |
<sup class="revenue-difference ${coProDifference >= 0 ? "positive" : "negative"}"> | |
(${coProDifference >= 0 ? "+" : ""}$${formatNumber(Math.abs(coProDifference))}) | |
</sup> | |
`; | |
} | |
totalsRow.cells[3].innerHTML = ` | |
<div class="revenue-wrapper"> | |
<strong class="revenue-amount">$${formatNumber(totalCoProExpenses)}</strong> | |
${differenceHtml} | |
</div> | |
`; | |
totalsRow.cells[4].innerHTML = `<strong>$${formatNumber(totalInternalExpenses)}</strong>`; | |
} | |
function formatNumber(num) { | |
return num.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, "$&,"); | |
} | |
function updateRow(cell) { | |
console.log("Updating row", cell); | |
const row = cell.closest("tr"); | |
const savelive = parseFloat(row.cells[1].textContent.replace(/[^\d.-]/g, "")); | |
const liveNation = parseFloat(row.cells[2].textContent.replace(/[^\d.-]/g, "")); | |
const internalExpenses = parseFloat(row.cells[4].textContent.replace(/[^\d.-]/g, "")); | |
const coProExpenses = savelive + liveNation; | |
const difference = coProExpenses - internalExpenses; | |
const expensesCell = row.cells[3]; | |
const currentClasses = expensesCell.className | |
.split(" ") | |
.filter((c) => c !== "positive" && c !== "negative"); | |
let differenceHtml = ""; | |
if (Math.abs(difference) > 0.01) { | |
differenceHtml = ` | |
<sup class="revenue-difference ${difference >= 0 ? "positive" : "negative"}"> | |
(${difference >= 0 ? "+" : ""}$${formatNumber(Math.abs(difference))}) | |
</sup> | |
`; | |
} | |
expensesCell.innerHTML = ` | |
<div class="revenue-wrapper"> | |
<span class="revenue-amount">$${formatNumber(coProExpenses)}</span> | |
${differenceHtml} | |
</div> | |
`; | |
expensesCell.className = [ | |
...currentClasses, | |
"revenue-diff", | |
difference >= 0 ? "positive" : "negative" | |
].join(" "); | |
updateTotals(); | |
} | |
editableCells.forEach((cell) => { | |
cell.addEventListener("click", function (e) { | |
console.log("Cell clicked", this); | |
e.stopPropagation(); | |
if (this.querySelector("input")) return; | |
const currentValue = this.textContent.replace(/[$,]/g, ""); | |
const input = document.createElement("input"); | |
input.type = "text"; | |
input.value = currentValue; | |
input.style.width = "100%"; | |
input.style.boxSizing = "border-box"; | |
input.style.textAlign = "right"; | |
this.textContent = ""; | |
this.appendChild(input); | |
input.focus(); | |
const blurHandler = function () { | |
const newValue = parseFloat(this.value.replace(/[^\d.-]/g, "")); | |
cell.textContent = isNaN(newValue) ? "$0.00" : "$" + formatNumber(newValue); | |
updateRow(cell); | |
}; | |
input.addEventListener("blur", blurHandler); | |
input.addEventListener("keydown", function (e) { | |
if (e.key === "Enter") { | |
this.blur(); | |
} | |
}); | |
document.addEventListener("click", function clickOutside(e) { | |
if (!cell.contains(e.target)) { | |
blurHandler.call(input); | |
input.removeEventListener("blur", blurHandler); | |
document.removeEventListener("click", clickOutside); | |
} | |
}); | |
}); | |
}); | |
costBasisDropdown.addEventListener("change", function () { | |
console.log("Cost basis changed", this.value); | |
const rows = coProTable.querySelectorAll("tbody tr:not(#totalsRow)"); | |
if (this.value === "artist") { | |
expensesColumnHeader.textContent = "External Expenses"; | |
} else { | |
expensesColumnHeader.textContent = "Internal Expenses"; | |
} | |
rows.forEach((row) => { | |
const itemName = row.cells[0].textContent; | |
const internalExpensesCell = row.cells[4]; | |
if (this.value === "artist" && artistSettlementCosts[itemName]) { | |
internalExpensesCell.textContent = artistSettlementCosts[itemName]; | |
} else if (this.value === "internal") { | |
internalExpensesCell.textContent = originalInternalCosts[itemName]; | |
} | |
updateRow(row.cells[1]); | |
}); | |
}); | |
// Initial update of all rows | |
editableCells.forEach((cell) => updateRow(cell)); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, | |
Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; | |
margin: 20px; | |
line-height: 1.5; | |
color: #333; | |
} | |
.controls { | |
margin-bottom: 20px; | |
display: flex; | |
align-items: center; | |
} | |
.controls label { | |
margin-right: 10px; | |
font-weight: 600; | |
color: #374151; | |
} | |
.controls select { | |
flex-grow: 1; | |
padding: 8px 12px; | |
border: 1px solid #d1d5db; | |
border-radius: 4px; | |
background-color: #f9fafb; | |
color: #111827; | |
font-size: 14px; | |
cursor: pointer; | |
} | |
table { | |
width: 100%; | |
border-collapse: collapse; | |
} | |
th, | |
td { | |
padding: 12px; | |
text-align: left; | |
border-bottom: 1px solid #e5e7eb; | |
} | |
th { | |
background-color: #f9fafb; | |
font-weight: 600; | |
color: #374151; | |
text-transform: uppercase; | |
font-size: 12px; | |
} | |
.right-align { | |
text-align: right; | |
} | |
.editable { | |
background-color: #ffffff; | |
cursor: pointer; | |
} | |
.disabled-column { | |
background-color: #f3f4f6; | |
color: #6b7280; | |
} | |
.revenue-wrapper { | |
display: flex; | |
flex-direction: column; | |
align-items: flex-end; | |
} | |
.revenue-amount { | |
font-size: 1em; | |
line-height: 1.2; | |
} | |
.revenue-difference { | |
font-size: 0.75em; | |
line-height: 1; | |
margin-top: 2px; | |
} | |
.revenue-difference.positive { | |
color: #10b981; | |
} | |
.revenue-difference.negative { | |
color: #ef4444; | |
} | |
@media screen and (max-width: 768px) { | |
.controls { | |
flex-direction: column; | |
align-items: flex-start; | |
} | |
.controls label { | |
margin-bottom: 5px; | |
} | |
.controls select { | |
width: 100%; | |
} | |
table { | |
border: 0; | |
} | |
table caption { | |
font-size: 1.3em; | |
} | |
table thead { | |
border: none; | |
clip: rect(0 0 0 0); | |
height: 1px; | |
margin: -1px; | |
overflow: hidden; | |
padding: 0; | |
position: absolute; | |
width: 1px; | |
} | |
table tr { | |
border-bottom: 3px solid #ddd; | |
display: block; | |
margin-bottom: 0.625em; | |
} | |
table td { | |
border-bottom: 1px solid #ddd; | |
display: block; | |
font-size: 0.8em; | |
text-align: right; | |
padding: 0.625em; | |
} | |
table td::before { | |
content: attr(data-label); | |
float: left; | |
font-weight: bold; | |
text-transform: uppercase; | |
} | |
table td:last-child { | |
border-bottom: 0; | |
} | |
.revenue-wrapper { | |
display: inline-flex; | |
flex-direction: column; | |
align-items: flex-end; | |
justify-content: flex-end; | |
} | |
.revenue-amount { | |
margin-right: 5px; | |
} | |
.revenue-difference { | |
font-size: 0.7em; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment