First column should be checkbox, so i can select rows from the table
Pagination is required and i should be able to change the number of rows per page
Header should be sticky if number of rows is more than 30
By clicking on the header column, i should be able to sort the table
Provide a button “Load All”, on click of load all button, implement virtualisation (docs: https://mui.com/material-ui/react-table/#virtualized-table) and load all the rows
Category: javascript
Category Added in a WPeMatico Campaign
why my code make reference error when i import the three.js , three is not defined while sript tag type= “module” Why?
browser cannot find a function named loadSVG in the current scope when the button’s onclick event is triggered.
Load SVG
import * as THREE from ‘https://unpkg.com/[email protected]/build/three.module.js’;
import { SVGLoader } from ‘https://unpkg.com/[email protected]/examples/jsm/loaders/SVGLoader.js’;
function renderSVGInThreeJS(svgData) {
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
const loader = new SVGLoader();
const svg = loader.parse(svgData); // Parse SVG into shapes
const material = new THREE.MeshStandardMaterial({ color: 0x999999 });
svg.paths.forEach(path => {
const shapes = path.toShapes(true);
shapes.forEach(shape => {
const extrudeSettings = { depth: 10, bevelEnabled: false };
const geometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
});
});
camera.position.z = 50;
const animate = function () {
requestAnimationFrame(animate);
renderer.render(scene, camera);
};
animate();
}
async function loadSVG(planId) {
const response = await fetch(`/api/floorplan/${planId}/`);
const data = await response.json();
if (data.svg) {
renderSVGInThreeJS(data.svg);
} else {
console.error(“Failed to load SVG”);
}
}
// ✅ Fix: Make loadSVG available globally
window.loadSVG = loadSVG;
expected to convert svg file with 2d svg img to 3d model file ,
tried es6 js components without bundler
globally declared the function name with window.loadSVG = loadSVG
but the errors come that uncaught type error , reference error , loadSVG is not defined
HTML – replace png image to webp [closed]
I have this css file:
.breadcrumbs .page-header {
padding: 120px 0 60px 0;
min-height: 20vh;
background: url(https://picsum.photos/500/200.webp) center bottom;
background-size: cover;
border-radius: 0px;
overflow: hidden;
position: relative;
}
<div class="breadcrumbs">
<div class="page-header">
...
</div>
</div>
but I don’t see the image page-header-bg.webp in the page, but with a PNG file is working filw
How to make the distance between the bottom label and the bottom the same as between the other labels
In ChartJs how to make the distance between the bottom label and the bottom the same as between the other labels. The first picture is what I have, the second is how it should be.
const chartOptions = ref({
responsive: true,
maintainAspectRatio: false,
events: [],
plugins: {
legend: { display: false },
tooltip: { enabled: false },
datalabels: {
anchor: 'end',
align: 'top',
clip: false,
color: (ctx) => (ctx.chart.data.labels[ctx.dataIndex] === today ? '#05F' : '#0C0055'),
font: {
family: 'Poppins',
size: 11, // doesn't work
weight: (ctx) => (ctx.chart.data.labels[ctx.dataIndex] === today ? '500' : 'normal'),
},
opacity: (ctx) => (ctx.chart.data.labels[ctx.dataIndex] === today ? 1 : 0.8),
formatter: (value) => value,
},
},
scales: {
x: {
position: 'bottom',
grid: {
borderDash: [9, 9],
drawBorder: true,
},
ticks: {
font: { family: 'Poppins', size: 11, weight: 500, style: 'normal' },
color: 'rgba(12, 0, 85, 0.5)',
lineHeight: 1.15,
},
},
y: {
beginAtZero: false,
grace: '0%',
min: newMinValue,
max: newMaxValue,
ticks: {
stepSize: stepSize,
callback: function (value) {
return value.toFixed(1) + (isKg.value ? ' kg' : 'lbs')
},
font: { family: 'Poppins', size: 11, weight: 500, style: 'normal' },
color: 'rgba(12, 0, 85, 0.5)',
lineHeight: 1.15,
},
grid: { borderDash: [8, 8], drawBorder: false },
},
},
})
Maybe someone else knows how to change the size of the dots, because the labels and the y-axis change at once
Thanks a lot
vue js unable to get values from button if SVG is present
I am using Vue3. I have a button that get the Id of user once click.
The problem is that if I place an svg icon within the button it causes the vue not to pick up the values of the data-id.. I.e
the data-id now returns null.
Below is the Laravel template blade:
@php $id = '999'; @endphp
<button id="profile"
v-on:click="start" :data-id="{{$id}}"
class="profile">
<svg class="svgclass h-12 w-12 text-red-500" fill="none" viewBox="0 0 24 24"
stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z"/>
</svg>
</button>
<script>
Vue.createApp({
data() {
return {
state: null,
};
},
mounted() {
console.log(this);
},
methods: {
start(event){
// const { id } = event.target.dataset;
var id = event.target.getAttribute('data-id');
console.log(id);
},
}
}).mount('#profile');
</script>
Why should we not use , with + for string concatenation in js?
A container contains 4 buttons with text A, B , C and D.
This works
let container = document.querySelector(".container");
container.addEventListener('click', (evt)=> {
if (evt.target.tagName == 'BUTTON') {
console.log("Button" + " " + evt.target.textContent + " got clicked");
}
})
output : Button A got clicked
This returns undefined
let container = document.querySelector(".container");
container.addEventListener('click', (evt)=> {
if (evt.target.tagName == 'BUTTON') {
console.log("Button" + " " + evt.target.textContent, + " got clicked");
}
})
output: Button A NaN
call Javascript from code behind in asp.net web form
I need help calling this javascript function after gridview2 is updated with information from a sql query. The gridview updates from the database no problem, and I can use a button to manually call the javascript function but I want to automatically call the function when the gridview is updated. The Javascript has to run on the client side, it adds objects to a three.js scene, the data in the gridview only relates to the file pathway, a unique object name and coordinates. I know I can add a event listener to other controls but I have no idea how to use an addeventlistener to a gridview object.
How do I get the client side javascript to run after the gridview is updated from the code behind?
function yourJavaScriptFunction(rowIndex) {
var x = rowIndex;
var file_path = getCellValue(x, 7); // Get the value from the second row, third column
if (file_path) {
console.log("Cell value: " + file_path);
} else {
console.log("Row or cell not found, get filepath."+ ' '+file_path);
alert("Row or cell not found,get filepath.");
}
var object_name = getCellValue(rowIndex, 1);
if (object_name) {
// alert(object_name);
console.log("Cell value: " + object_name);
} else {
console.log("Row or cell not found, get object name.");
alert("Row or cell not found, get object name.");
}
// alert('Button in row ' + rowIndex + ' clicked!');
//alert(value);
module.add_Object(file_path, object_name);
//add_Object();
// mytest.add_Object();
return false; // Prevent postback if needed
}
<asp:GridView ID="GridView2" runat="server" Height="100%" Width="100%" HorizontalAlign="Center" AutoGenerateColumns="False" DataSourceID="SqlDataSource3" >
<Columns>
<asp:BoundField DataField="NAME" HeaderText="NAME" SortExpression="NAME" />
<asp:BoundField DataField="ID" HeaderText="ID" SortExpression="ID" />
<asp:BoundField DataField="SUBTYPE1" HeaderText="SUBTYPE1" SortExpression="SUBTYPE1" />
<asp:BoundField DataField="IMAGE_FILE_PATH" HeaderText="IMAGE_FILE_PATH" SortExpression="IMAGE_FILE_PATH" />
<asp:BoundField DataField="TYPE" HeaderText="TYPE" SortExpression="TYPE" />
<asp:BoundField DataField="COUNT" HeaderText="COUNT" SortExpression="COUNT" />
<asp:BoundField DataField="LOCATION" HeaderText="LOCATION" SortExpression="LOCATION" />
<asp:BoundField DataField="TRADE_SIZE" HeaderText="TRADE_SIZE" SortExpression="TRADE_SIZE" />
</Columns>
<EditRowStyle HorizontalAlign="Left" />
</asp:GridView>
</ContentTemplate>
</asp:UpdatePanel>
</div>
Is there any way to get the p5.js element to inherit styles without changing the draw function?
I’m working on a p5.js project and trying to apply styling rules to my canvas via CSS, but nothing seems to work,
the only option seems to set all the styling one at a time inside the draw() function, but I have so many draw functions in my app for different visuals I don’t want to spend forever on it, why doesn’t CSS affect a p5.js canvas?
NextJS 15 Build Error occurred prerendering page “/404”
when running NODE_ENV=developement npm run build in nextjs project, there is the following error,
Error: should not be import outside of pages/_document.
Read more: https://nextjs.org/docs/messages/no-document-import-in-page
at y (D:….nextserverchunks7627.js:6:1263)
An error occurred while pre-rendering the page “/404”. Read more: https://nextjs.org/docs/messages/prerender-error
Error: should not be import outside of pages/_document.
Read more: https://nextjs.org/docs/messages/no-document-import-in-page
in Y (D:…node_modulesnextdistcompilednext-serverpages.runtime.prod.js:16:5469)
in y (D:….nextserverchunks7627.js:6:1263)
in react-stack-bottom-frame (D:…node_modulesreact-domcjsreact-dom-server.edge.development.js:8798:18)
in renderWithHooks (D:…node_modulesreact-domcjsreact-dom-server.edge.development.js:4722:19)
in renderElement (D:…node_modulesreact-domcjsreact-dom-server.edge.development.js:5157:23)
in retryNode (D:…node_modulesreact-domcjsreact-dom-server.edge.development.js:5805:22)
in renderNodeDestructive (D:…node_modulesreact-domcjsreact-dom-server.edge.development.js:5631:11)
in renderElement (D:…node_modulesreact-domcjsreact-dom-server.edge.development.js:5143:11)
in retryNode (D:…node_modulesreact-domcjsreact-dom-server.edge.development.js:5805:22)
in renderNodeDestructive (D:…node_modulesreact-domcjsreact-dom-server.edge.development.js:5631:11)
Export encountered an error at /_error: /404, exiting the build process.
⨯ Next.js build worker exited with code: 1 and signal: null
I didn’t create a custom 404 page. Please help.
NextJS 15.1.7, React 19.0.0, tailwindcss 4.0.7, DaisyUI 5.0.0, Prisma 6.4.1
d3.js force simulation: Some edges not rendering on double-click expansion
I’m building a topology visualization using d3.js where nodes are connected by multiple edges. I implemented a feature to toggle an “expanded” state on a group of edges by double-clicking, which applies an offset to the edge paths (using d3.curveCatmullRom with an alpha of 0.3) so that multiple edges between the same node pair become visible as separate curves.
However, when I double-click on an edge group (for example, between “50A_1_EMS” and “3000A_1_EMS”), only one edge is visible while the other remains hidden, even though two edges exist.
Here’s what I’ve done so far:
In the double-click event handler, I call d3.select(event.currentTarget).raise() to bring the clicked edge group to the top.
In the tick function, after updating the edge positions, I filter for expanded edge groups and call .raise() on them:
linkGroup
.selectAll("g.link-group")
.filter((d) => linkGroupState.get(d.key))
.raise();
and
function ticked() {
// 엣지 그룹 업데이트: 선 경로 및 라벨 위치 계산
linkGroup.selectAll("g.link-group").each(function (d) {
const source = d.source;
const target = d.target;
const dx = target.x - source.x;
const dy = target.y - source.y;
// 기본 중간점(노드 중심)
const centerX = (source.x + target.x) / 2;
const centerY = (source.y + target.y) / 2;
// expanded 상태라면 오프셋을 적용하여 곡선의 중간점 계산
let midX = centerX;
let midY = centerY;
const expanded = linkGroupState.get(d.key) || false;
if (expanded) {
const offsetFactor = 20;
const offset = (d.linkIndex - (d.totalLinks - 1) / 2) * offsetFactor;
const norm = Math.sqrt(dx * dx + dy * dy);
if (norm !== 0) {
const px = -dy / norm;
const py = dx / norm;
midX += px * offset;
midY += py * offset;
}
}
const lineGenerator = d3.line().curve(d3.curveCatmullRom.alpha(0.3));
const coords = [
[source.x, source.y],
[midX, midY],
[target.x, target.y],
];
const pathData = lineGenerator(coords);
// 여기서 디버깅용으로 pathData 출력
console.log(
"DEBUG pathData:",
d.key,
"linkIndex:",
d.linkIndex,
pathData
);
d3.select(this).select("path.link-visible").attr("d", pathData);
d3.select(this).select("path.link-hit").attr("d", pathData);
d3.select(this)
.select("g.link-label-group")
.attr("transform", `translate(${centerX}, ${centerY})`);
});
// 노드 위치 업데이트
nodeGroup
.selectAll("g")
.attr("transform", (d) => `translate(${d.x}, ${d.y})`);
updateVisibleNodes();
// 여기서 엣지 라벨 위치 업데이트 추가
edgeLabelLayer.selectAll("g.link-label-group").each(function (d) {
const centerX = (d.source.x + d.target.x) / 2;
const centerY = (d.source.y + d.target.y) / 2;
// linkGroupState에 저장된 상태(true: expanded, false: collapsed)
const isExpanded = linkGroupState.get(d.key) || false;
d3.select(this)
.attr("transform", `translate(${centerX}, ${centerY})`)
.style("display", isExpanded ? "none" : "block"); // expanded 상태면 숨김
});
// 4) 속도 라벨(각 링크별) 위치 + 표시 여부
edgeSpeedLayer.selectAll("g.link-speed-label").each(function (d) {
console.log(
"Speed label:",
d,
"isExpanded:",
linkGroupState.get(d.key)
);
const centerX = (d.source.x + d.target.x) / 2;
const centerY = (d.source.y + d.target.y) / 2;
const isExpanded = linkGroupState.get(d.key) || false;
d3.select(this)
.attr("transform", `translate(${centerX}, ${centerY})`)
.style("display", isExpanded ? "block" : "none") // 펼쳐졌으면 보이기
.select("text.speed-label-text")
.text(d.eth_speed ? d.eth_speed + "G" : "");
// eth_speed가 있다면 "1G", "10G" 등 표시
});
linkGroup.lower();
nodeGroup.raise();
linkGroup
.selectAll("g.link-group")
.filter((d) => linkGroupState.get(d.key))
.raise();
// 디버깅용: DOM 순서 로그 출력
const order = linkGroup
.selectAll("g.link-group")
.nodes()
.map((node) => node.__data__.key);
console.log("현재 link-group 순서:", order);
}
I log the computed SVG path data (pathData), keys, and linkIndex values. The pathData outputs (e.g., M905.596,254.625C…) appear normal and there are no NaN or Infinity values.
Potential causes I suspect are:
The multiple edges between the same node pair are grouped using a key generated by [source, target].sort().join(“|”), so even if they have different linkIndex values, they share the same key. This might be causing the double-click toggle to affect all edges simultaneously.
The simulation’s tick or transition updates might be inadvertently reordering the DOM elements after the double-click event.
I’ve tried adjusting the offset factor and verifying the DOM order, but the issue persists.
Has anyone encountered a similar problem or can suggest a workaround (for instance, including the linkIndex in the key or another approach) to ensure that all edges become visible on double-click expansion?
Thanks in advance!
How to check new line character in javascript
I need want to check the new line in given string
the original value
the image contain original string value in profile text
when i read the profile text value
var index = jQuery.inArray(dyanmic profile id, profilename);
namevalues[index] = $find(id).get_text();
i got like this
“MEWUJUDKAN JAWATAN PEMANDUGRED R10 KONTRAK BAGI TIMBALAN PENGERUSI MAJLIS AGAMA ISLAMMELAKA DAN PERLANTIKAN ENCIK ADIBIN ALI, PEMANDU GRED R10 SECARA KONTRAK MAJLIS AGAMA ISLAMMELAKA.”
the new line remove the space and join the two string.
The image has wrong displays on Iphone
I am creating a website with parallax scroll effects but on Iphone the parallax images do not seems good it seems like they are of bad quality and incorrect cropping I tried to use <figure> but the issue stills the same result, also I tried to overwrite the images on mobiles to more adaptability this also do not works.
Here is my current code:
document.addEventListener("scroll", function() {
const sections = document.querySelectorAll('.section-background');
sections.forEach(section => {
const img = section.querySelector('.parallax-img');
const rect = section.getBoundingClientRect();
const sectionHeight = section.offsetHeight;
// Calculate progress (0 when the section is fully below, 1 when it's fully above)
let progress = Math.min(Math.max(-rect.top / sectionHeight, 0), 1);
// Map progress to translation from 0 to -sectionHeight
const translateY = -progress * sectionHeight;
img.style.transform = `translateY(${translateY}px)`;
});
});
.ah2 {
background: rgba(128,128,128,.5);
font-size: 125px;
padding-top: 0.25em;
font-size: 28pt !important;
width: 60%
}
.section-background {
position: relative;
height: 100vh;
width: 100%;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.parallax-img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 200%; /* twice the section height ensures full coverage */
object-fit: cover;
z-index: -1;
will-change: transform;
margin: 0;
}
figure > img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%; /* twice the section height ensures full coverage */
object-fit: cover;
z-index: -1;
will-change: transform;
}
<div class="section-background">
<figure class="parallax-img">
<img src="https://a.travel-assets.com/findyours-php/viewfinder/images/res70/363000/363235-Nice.jpg"></img>
</figure>
<div class="content">
<h2 class="ah2">WHERE EXCEPTIONAL ENTERTAINMENT — MUSIC, THEATRE, THE ARTS AND INSPIRING LECTURES — ENRICHES OUR CULTURE.</h2>
</div>
</div>
<div class="section-background">
<figure class="parallax-img">
<img className="parallax-img" src="https://static.vecteezy.com/system/resources/thumbnails/040/890/255/small_2x/ai-generated-empty-wooden-table-on-the-natural-background-for-product-display-free-photo.jpg"></img>
</figure>
<div class="content">
<h2 class="ah2">WHERE SHARING A GOOD LAUGH HELPS BUILD FRIENDSHIPS THAT LAST A LIFETIME.</h2>
</div>
</div>
<div class="section-background">
<figure class="parallax-img">
<img src="https://smart-tourism-capital.ec.europa.eu/sites/default/files/styles/embed_large/public/2021-09/Nice2.jpg?itok=3GEJXlT6"></img>
</figure>
<div class="content">
<h2 class="ah2">WHERE MUSIC UNDER THE REDWOODS CREATES A MAGIC SHARED WITH FRIENDS AND FAMILY.</h2>
</div>
</div>
Expected result
Current result
Firebase realtimedatabase sort to time
I’m writing in the realtime database using the push() function
The push() function orders the input data in chronological order, but it does not, and it orders the data in numerical order
I want the data to be entered in chronological order
The userName is not based on timestamp, but the data is recorded in the order of 0107 after 0104
"0104": {
"-OKQu5uz3y6aq_exRrwA": {
"contents": "1",
"timestamp": "2025-03-03T12:08:34.229Z",
"userName": "0104"
}
},
"0107": {
"-OHJRj4ye8DOxw0UbEo1": {
"contents": "1",
"timestamp": "2025-01-23T18:35:48.476Z",
"userName": "0107"
}
},
"0107": {
"-OJRQ1IGgYYgmk-ri7Ye": {
"contents": "1 ",
"timestamp": "2025-02-19T04:16:44.235Z",
"userName": "0107"
},
<script type="module">
import { initializeApp } from "https://www.gstatic.com/firebasejs/11.1.0/firebase-app.js";
import { getDatabase, ref, set, get, child, push } from "https://www.gstatic.com/firebasejs/11.1.0/firebase-database.js";
import { getAnalytics } from "https://www.gstatic.com/firebasejs/11.1.0/firebase-analytics.js";
const firebaseConfig = {My Firebase Info};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
const analytics = getAnalytics(app);
const db = getDatabase(app);
document.getElementById("submit").addEventListener('click', function (e) {
e.preventDefault();
const currentTime = new Date().toISOString(); // Get current time in ISO format
push(ref(db, 'user/' + document.getElementById("userName").value), {
userName: document.getElementById("userName").value,
contents: document.getElementById("contents").value,
timestamp: currentTime // Add timestamp
});
alert("Success");
});
</script>
How to insert a note node in the DOM at the top of any web page?
I try to add a text box if the REQUEST_URI have an entry note like this:
By example: https://stackoverflow.com/?note=TEST don’t produce any new node from my code.
// ==UserScript==
// @name Simple note from QUERY_STRING
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Affiche la valeur de 'note' de la query string de l'URL.
// @author Mévatlavé
// @match *://*/*
// @grant none
// ==/UserScript==
(function() {
'use strict';
function addNoteHeader(noteValue) {
const header = document.createElement('div');
header.className = 'query-string-notifier';
header.textContent = noteValue;
header.style.backgroundColor = 'yellow';
header.style.color = 'black';
header.style.fontSize = '24px';
header.style.fontWeight = 'bold';
header.style.textAlign = 'center';
header.style.padding = '10px';
header.style.position = 'fixed';
header.style.top = '0';
header.style.left = '0';
header.style.right = '0';
header.style.zIndex = '1000';
document.body.insertBefore(header, document.body.firstChild);
}
const urlParams = new URLSearchParams(window.location.search);
const noteValue = urlParams.get('note');
if (noteValue) {
addNoteHeader(noteValue);
}
})();
It works on some sites, but not others. What’s the cause?
Is my issue is CSP or DOM loading related?
How to fix or workaround to work on any website?
Submit Button wont work on my submission form
I have a googlesheet to help track my expenses for my Turo Business. I created a form that I upload a receipt and fill in the info related to that receipt.
Everything works great however when I fill out my form and press submit nothing happens. I can see that it creates the Folders needed in my GoogleDrive to store and save the receipts, but it stops there and doesn’t actually save the receipt and it doesn’t take the data I entered on the form and save it in the Submission Data sheet. It just allows me to keep pressing the Submit Button.
Here is a link to a sample sheet: https://docs.google.com/spreadsheets/d/1qR_EmTpHMhS6fycu47WnWO-3tGHkoAphzXbHxvCX96c/edit?usp=drive_link
Code.gs:
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('Turo Tools')
.addItem('Upload Receipt', 'showDialog')
.addToUi();
}
function showDialog() {
const html = HtmlService.createHtmlOutputFromFile('Dialog')
.setWidth(1800)
.setHeight(1600);
SpreadsheetApp.getUi().showModalDialog(html, 'Upload Receipt');
}
function getDropdownData() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Dropdowns');
const ranges = ['Category', 'Payment Method', 'Tax-Deductible', 'Account'];
const data = {};
ranges.forEach(range => {
const headerRow = sheet.getRange("1:1").getValues()[0]; // Get header row
const columnIndex = headerRow.indexOf(range) + 1; // Find column index for the range name
if (columnIndex > 0) {
const rangeValues = sheet.getRange(2, columnIndex, sheet.getLastRow() - 1).getValues().flat().filter(cell => cell !== '');
data[range] = rangeValues;
}
});
return data;
}
// Pull Vehicle, Guest Name, and Trip ID from 'CSV Data' sheet
function getCSVData() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('CSV Data');
const data = sheet.getDataRange().getValues();
const vehicles = [];
const guestNames = [];
const tripIds = [];
const vehicleGuestMap = {}; // Maps vehicles to guest names
const tripIDGuestMap = {}; // Maps vehicle -> guest -> trip ID
const vehicleColIndex = 4; // Column E (index 4)
const guestNameColIndex = 2; // Column C (index 2)
const tripIdColIndex = 1; // Column B (index 1)
for (let i = 2; i < data.length; i++) {
const vehicle = data[i][vehicleColIndex];
const guestName = data[i][guestNameColIndex];
const tripId = String(data[i][tripIdColIndex]); // Ensure trip ID is a string
if (vehicle && !vehicles.includes(vehicle)) {
vehicles.push(vehicle);
}
if (guestName && !guestNames.includes(guestName)) {
guestNames.push(guestName);
}
if (tripId && !tripIds.includes(tripId)) {
tripIds.push(tripId);
}
// Map vehicles to guest names
if (vehicle) {
if (!vehicleGuestMap[vehicle]) {
vehicleGuestMap[vehicle] = new Set();
}
vehicleGuestMap[vehicle].add(guestName);
}
// Map vehicles and guest names to trip IDs
if (vehicle && guestName && tripId) {
if (!tripIDGuestMap[vehicle]) {
tripIDGuestMap[vehicle] = {};
}
if (!tripIDGuestMap[vehicle][guestName]) {
tripIDGuestMap[vehicle][guestName] = [];
}
tripIDGuestMap[vehicle][guestName].push(tripId);
}
}
// Convert Sets to arrays for JSON compatibility
for (let vehicle in vehicleGuestMap) {
vehicleGuestMap[vehicle] = Array.from(vehicleGuestMap[vehicle]);
}
// Ensure the tripIDGuestMap is returned with the correct structure
Logger.log({ vehicles, guestNames, tripIds, vehicleGuestMap, tripIDGuestMap });
return {
vehicles,
guestNames,
tripIds,
vehicleGuestMap,
tripIDGuestMap
};
}
function saveReceipt(formData, base64File) {
try {
const driveFolderId = '1pN7V57tRqhf1LU_M5rZlB8YJMNGSwAPu'; // Turo folder ID
const driveFolder = DriveApp.getFolderById(driveFolderId);
const date = new Date(formData.date);
const year = date.getFullYear();
const month = Utilities.formatDate(date, Session.getScriptTimeZone(), 'MMMM');
let receiptsFolder = driveFolder.getFoldersByName('Receipts');
receiptsFolder = receiptsFolder.hasNext() ? receiptsFolder.next() : driveFolder.createFolder('Receipts');
let yearFolder = receiptsFolder.getFoldersByName(year.toString());
yearFolder = yearFolder.hasNext() ? yearFolder.next() : receiptsFolder.createFolder(year.toString());
let monthFolder = yearFolder.getFoldersByName(month);
monthFolder = monthFolder.hasNext() ? monthFolder.next() : yearFolder.createFolder(month);
let categoryFolder = monthFolder.getFoldersByName(formData.category);
categoryFolder = categoryFolder.hasNext() ? categoryFolder.next() : monthFolder.createFolder(formData.category);
// Handle Base64 file properly
const base64Parts = base64File.split(',');
if (base64Parts.length < 2) {
throw new Error("Invalid base64 file format");
}
const contentType = base64Parts[0].match(/:(.*?);/)[1];
const decodedFile = Utilities.base64Decode(base64Parts[1]);
const uniqueId = Utilities.getUuid();
const fileName = `${formData.vendor}_${formData.date}_${uniqueId}`;
const fileBlob = Utilities.newBlob(decodedFile, contentType, fileName);
const savedFile = categoryFolder.createFile(fileBlob);
const fileUrl = savedFile.getUrl();
console.log("File successfully saved:", fileUrl);
// Save submission data
const submissionSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Submission Data');
const lastRow = submissionSheet.getLastRow();
const firstEmptyRow = lastRow + 1; // Ensures data is written in the next available row
submissionSheet.getRange(firstEmptyRow, 1, 1, 19).setValues([[
formData.date,
formData.category,
formData.description,
formData.vendor,
formData.paymentMethod,
formData.account,
formData.amount.subtotal,
formData.amount.salesTax,
formData.amount.useTax,
formData.amount.liquorTax,
formData.amount.fee,
formData.amount.tip,
formData.amount.total,
formData.taxDeductible,
formData.vehicle,
formData.guestName,
formData.tripId,
fileUrl,
formData.notes
]]);
console.log("Form data successfully recorded in the spreadsheet.");
return 'File saved and data recorded successfully!';
} catch (error) {
console.error("Error in saveReceipt:", error);
return `Error: ${error.message}`;
}
Dialog.html
<!DOCTYPE html>
<html>
<head>
<title>Upload Receipt</title>
<style>
body {
font-family: Arial, sans-serif;
color: #333;
margin: 0;
padding: 20px;
background-color: #f9f9f9;
}
.container {
display: flex;
flex-wrap: wrap;
gap: 20px;
background: #fff;
border-radius: 10px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.preview {
flex: 1;
padding: 20px;
background-color: #f4f4f4;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.preview h3 {
margin-bottom: 15px;
}
.form {
flex: 2;
padding: 10px;
display: grid;
grid-template-columns: repeat(1, 1fr);
gap: 10px;
}
input, select, textarea {
width: 150px;
margin-bottom: 15px;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 14px;
font-family: Arial, sans-serif;
color: #333;
background-color: #fff;
}
button {
padding: 12px 20px;
font-size: 16px;
color: #fff;
background-color: #4CAF50;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #45a049;
}
iframe {
border: 1px solid #ddd;
border-radius: 5px;
width: 400px;
height: 800px;
}
.amount-grid {
display: grid;
grid-template-columns: repeat(2, 10fr);
gap: 10px;
}
.trip-grid {
display: grid;
grid-template-columns: repeat(3, 10fr);
gap: 10px;
}
.receipt-grid {
display: grid;
grid-template-columns: repeat(2, 10fr);
gap: 10px;
}
.notes-grid {
display: grid;
grid-template-columns: repeat(1, 10fr);
gap: 10px;
}
</style>
</head>
<body>
<div class="container">
<div class="preview">
<h3>Preview</h3>
<input type="file" id="fileInput" accept=".pdf,.jpg,.jpeg" />
<img id="filePreview" style="width: 100%; height: 800px; object-fit: contain; border: 1px solid #ddd; border-radius: 5px;" />
</div>
<div class="form">
<div>
<h4>Trip Details</h4>
<div class="trip-grid">
<select id="vehicle" onchange="updateGuestNameDropdown()" required></select>
<select id="guestName" onchange="updateTripIdDropdown()" required></select>
<select id="tripId" required></select>
</div>
</div>
<div>
<h4>Receipt Details</h4>
<div class="receipt-grid">
<input type="date" id="date" placeholder="Date" required />
<select id="category" required></select>
<input type="text" id="description" placeholder="Description" />
<input type="text" id="vendor" placeholder="Vendor" required />
<select id="paymentMethod" required></select>
<select id="account" required></select>
<select id="taxDeductible" required></select>
</div>
</div>
<div>
<h4>Amount</h4>
<div class="amount-grid">
<input type="number" id="subtotal" placeholder="Subtotal" oninput="updateTotal()" />
<input type="number" id="salesTax" placeholder="Sales Tax" oninput="updateTotal()" />
<input type="number" id="useTax" placeholder="Use Tax" oninput="updateTotal()" />
<input type="number" id="liquorTax" placeholder="Liquor Tax" oninput="updateTotal()" />
<input type="number" id="fee" placeholder="Fee" oninput="updateTotal()" />
<input type="number" id="tip" placeholder="Tip" oninput="updateTotal()" />
</div>
<input type="number" id="total" placeholder="Total" readonly />
</div>
<div>
<div class="notes-grid">
<textarea id="notes" placeholder="Notes"></textarea>
</div>
</div>
<button onclick="submitForm()">Submit</button>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
google.script.run
.withSuccessHandler(populateDropdowns)
.getDropdownData();
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', function () {
const file = fileInput.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function (e) {
const preview = document.getElementById('filePreview');
preview.src = e.target.result;
preview.alt = 'File Preview';
};
reader.readAsDataURL(file);
}
});
});
function populateDropdowns(data) {
const dropdownMapping = {
category: { key: 'Category', placeholder: 'Category' },
paymentMethod: { key: 'Payment Method', placeholder: 'Payment Method' },
taxDeductible: { key: 'Tax-Deductible', placeholder: 'Tax Deductible' },
account: { key: 'Account', placeholder: 'Account' }
};
Object.entries(dropdownMapping).forEach(([dropdownId, { key, placeholder }]) => {
const dropdown = document.getElementById(dropdownId);
if (data[key]) {
dropdown.innerHTML = ''; // Clear existing options
const defaultOption = document.createElement('option');
defaultOption.value = '';
defaultOption.textContent = placeholder;
defaultOption.disabled = true;
defaultOption.selected = true;
dropdown.appendChild(defaultOption);
// Add actual options
data[key].forEach(item => {
const option = document.createElement('option');
option.value = item;
option.textContent = item;
dropdown.appendChild(option);
});
}
});
// Update the dropdowns for vehicle, guest, and trip ID from CSV data
updateVehicleDropdown();
}
function updateVehicleDropdown() {
const vehicleDropdown = document.getElementById('vehicle');
google.script.run.withSuccessHandler(function(csvData) {
if (!csvData.vehicles || csvData.vehicles.length === 0) {
console.log("No vehicles found.");
return;
}
vehicleDropdown.innerHTML = '<option value="">Select Vehicle</option>';
csvData.vehicles.forEach(vehicle => {
const option = document.createElement('option');
option.value = vehicle;
option.textContent = vehicle;
vehicleDropdown.appendChild(option);
});
}).getCSVData();
}
function updateGuestNameDropdown() {
const vehicle = document.getElementById('vehicle').value;
const guestNameDropdown = document.getElementById('guestName');
google.script.run.withSuccessHandler(function(csvData) {
// Check if vehicleGuestMap exists and has data
if (!csvData.vehicleGuestMap || !csvData.vehicleGuestMap[vehicle]) {
console.log("No guests found for the selected vehicle.");
return;
}
// Get the guests for the selected vehicle from the vehicleGuestMap
const guestsForVehicle = csvData.vehicleGuestMap[vehicle];
// Clear and repopulate the dropdown
guestNameDropdown.innerHTML = '<option value="">Select Guest</option>';
guestsForVehicle.forEach(guest => {
const option = document.createElement('option');
option.value = guest;
option.textContent = guest;
guestNameDropdown.appendChild(option);
});
// Log guests for debugging
console.log("Guests for selected vehicle:", guestsForVehicle);
}).getCSVData();
}
function updateTripIdDropdown() {
const vehicle = document.getElementById('vehicle').value;
const guestName = document.getElementById('guestName').value;
const tripIdDropdown = document.getElementById('tripId');
google.script.run.withSuccessHandler(function(csvData) {
if (!csvData.tripIDGuestMap || Object.keys(csvData.tripIDGuestMap).length === 0) {
console.log("No trip IDs found.");
return;
}
const tripsForVehicleGuest = csvData.tripIDGuestMap[vehicle] && csvData.tripIDGuestMap[vehicle][guestName] || [];
tripIdDropdown.innerHTML = '<option value="">Select Trip ID</option>';
tripsForVehicleGuest.forEach(tripId => {
const option = document.createElement('option');
option.value = tripId;
option.textContent = tripId;
tripIdDropdown.appendChild(option);
});
if (tripsForVehicleGuest.length === 0) {
console.log(`No trip IDs found for vehicle: ${vehicle} and guest: ${guestName}`);
}
}).getCSVData();
}
function submitForm() {
const formData = {
date: document.getElementById('date').value,
category: document.getElementById('category').value,
description: document.getElementById('description').value,
vendor: document.getElementById('vendor').value,
paymentMethod: document.getElementById('paymentMethod').value,
account: document.getElementById('account').value,
taxDeductible: document.getElementById('taxDeductible').value,
vehicle: document.getElementById('vehicle').value,
guestName: document.getElementById('guestName').value,
tripId: document.getElementById('tripId').value,
amount: {
subtotal: document.getElementById('subtotal').value,
salesTax: document.getElementById('salesTax').value,
useTax: document.getElementById('useTax').value,
liquorTax: document.getElementById('liquorTax').value,
fee: document.getElementById('fee').value,
tip: document.getElementById('tip').value,
total: document.getElementById('total').value,
},
notes: document.getElementById('notes').value,
};
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = function (e) {
const base64File = e.target.result.split(',')[1];
google.script.run.saveReceipt(formData, base64File);
};
reader.readAsDataURL(file);
}
function updateTotal() {
const subtotal = parseFloat(document.getElementById('subtotal').value) || 0;
const salesTax = parseFloat(document.getElementById('salesTax').value) || 0;
const useTax = parseFloat(document.getElementById('useTax').value) || 0;
const liquorTax = parseFloat(document.getElementById('liquorTax').value) || 0;
const fee = parseFloat(document.getElementById('fee').value) || 0;
const tip = parseFloat(document.getElementById('tip').value) || 0;
const total = subtotal + salesTax + useTax + liquorTax + fee + tip;
document.getElementById('total').value = total.toFixed(2);
}
</script>
</body>
</html>


