export file frontend

This commit is contained in:
2025-07-23 00:28:00 +08:00
parent 7a99219917
commit c8eab1f0f6
3 changed files with 230 additions and 32 deletions

View File

@@ -19,7 +19,8 @@
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.2.6"
"vite": "^6.2.6",
"xlsx": "^0.18.5"
},
"dependencies": {
"@supabase/ssr": "^0.6.1",

View File

@@ -2,6 +2,7 @@
import { supabase } from "$lib/supabaseClient";
import { onMount } from "svelte";
import { v4 as uuidv4 } from "uuid";
import * as XLSX from 'xlsx';
type Timesheets = {
id: number;
@@ -447,44 +448,182 @@
async function exportTimesheets() {
if (!selectedMonth || !selectedYear) {
alert("Please select a month and year to export.");
alert("Please select a month and year to export.");
return;
}
try {
const response = await fetch("https://flow.catalis.app/webhook-test/villabugis-timesheets", {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
month: selectedMonth,
year: selectedYear
})
});
if (!response.ok) {
throw new Error(`Export failed: ${response.statusText}`);
}
const randomUuid = uuidv4().toString();
// Jika response adalah file (application/octet-stream atau Excel)
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `timesheet-${selectedYear}-${String(selectedMonth).padStart(2, '0')}-${randomUuid}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error("Export error:", error);
alert("Failed to export timesheet.");
}
}
async function exportToExcelFromFrontend() {
if (!selectedMonth || !selectedYear) {
alert("Please select both month and year");
return;
}
try {
const response = await fetch("https://flow.catalis.app/webhook-test/villabugis-timesheets", {
method: "PATCH",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
month: selectedMonth,
year: selectedYear
})
const startDate = new Date(selectedYear, selectedMonth - 1, 1);
const endDate = new Date(selectedYear, selectedMonth, 0, 23, 59, 59);
// 1. Fetch timesheet data
const { data: timesheetData, error: timesheetErr } = await supabase
.from("vb_timesheet_data")
.select(`
id,
work_description,
total_work_hour,
type_of_work,
category_of_work,
approval,
datetime_in,
datetime_out,
remarks,
approved_date,
villa_name,
entered_name,
approved_name
`)
.gte("datetime_in", startDate.toISOString())
.lte("datetime_in", endDate.toISOString());
if (timesheetErr) {
console.error("Error fetching timesheet:", timesheetErr);
alert("Failed to fetch timesheet data.");
return;
}
// 2. Fetch active villas
const { data: villaData, error: villaErr } = await supabase
.from("vb_villas")
.select("villa_name")
.eq("villa_status", "Active");
if (villaErr) {
console.error("Error fetching villas:", villaErr);
alert("Failed to fetch villas.");
return;
}
const categoryHeaders = [
"Cleaning",
"Gardening/Pool",
"Maintenance",
"Supervision",
"Guest Service",
"Administration",
"Non Billable"
];
const sheet2Name = "Data";
const ws2 = XLSX.utils.json_to_sheet(timesheetData || []);
// 3. Build Sheet1 rows
const sheet1Rows: any[][] = [];
// A1:B1 → Period label
const periodText = `${startDate.toLocaleString("default", { month: "short" })}/${selectedYear}`;
sheet1Rows.push(["Period", periodText]);
// A2:I2 → Headers
const headers = ["Villa Name", ...categoryHeaders, "TOTAL"];
sheet1Rows.push(headers);
// A3:A(n) → Villa rows
villaData?.forEach((villa, idx) => {
const excelRow = idx + 3; // Excel row number
const row: any[] = [villa.villa_name];
// BH formulas
categoryHeaders.forEach((cat, catIdx) => {
const colLetter = String.fromCharCode(66 + catIdx); // 'B' = 66
const formula = `SUMIFS(${sheet2Name}!$C:$C, ${sheet2Name}!$K:$K, $A${excelRow}, ${sheet2Name}!$E:$E, ${colLetter}$2, ${sheet2Name}!$F:$F, TRUE)`;
row.push({ f: formula });
});
if (!response.ok) {
throw new Error(`Export failed: ${response.statusText}`);
// I column (TOTAL)
row.push({ f: `SUM(B${excelRow}:H${excelRow})` });
sheet1Rows.push(row);
});
// Horizontal summary beside the villa table
const headerRowIdx = 1;
const officeRowIdx = 2;
const fbRowIdx = 3;
const villasOnlyRowIdx = 4;
const ensureCols = (row: any[], length: number) => {
while (row.length < length) row.push(null);
return row;
};
[headerRowIdx, officeRowIdx, fbRowIdx, villasOnlyRowIdx].forEach((idx) => {
sheet1Rows[idx] = sheet1Rows[idx] || [];
ensureCols(sheet1Rows[idx], 13); // at least to column M
});
// Insert labels and formulas
sheet1Rows[headerRowIdx][10] = "Total Work Hours"; // K2
sheet1Rows[headerRowIdx][11] = { f: `SUM(${sheet2Name}!$C:$C)` }; // L2
sheet1Rows[officeRowIdx][10] = "Office"; // K3
sheet1Rows[officeRowIdx][11] = { f: `SUMIFS(${sheet2Name}!$C:$C, ${sheet2Name}!$K:$K, K3)` }; // L3
sheet1Rows[fbRowIdx][10] = "FB"; // K4
sheet1Rows[fbRowIdx][11] = { f: `SUMIFS(${sheet2Name}!$C:$C, ${sheet2Name}!$K:$K, K4)` }; // L4
sheet1Rows[villasOnlyRowIdx][10] = "Villas Only"; // K5
sheet1Rows[villasOnlyRowIdx][11] = { f: `L2-L3` }; // L5
// Convert to worksheet
const ws1 = XLSX.utils.aoa_to_sheet(sheet1Rows);
// Bold header row (A2:I2)
for (let c = 0; c <= headers.length; c++) {
const cellRef = XLSX.utils.encode_cell({ r: 1, c });
if (ws1[cellRef]) {
ws1[cellRef].s = { font: { bold: true } };
}
const randomUuid = uuidv4().toString();
// Jika response adalah file (application/octet-stream atau Excel)
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `timesheet-${selectedYear}-${String(selectedMonth).padStart(2, '0')}-${randomUuid}.xlsx`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error("Export error:", error);
alert("Failed to export timesheet.");
}
}
// 4. Build Workbook
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws1, "Summary");
XLSX.utils.book_append_sheet(wb, ws2, sheet2Name);
// 5. Export
const filename = `timesheet_${selectedYear}-${String(selectedMonth).padStart(2, "0")}.xlsx`;
XLSX.writeFile(wb, filename);
}
// Function to delete a timesheet
async function deleteTimesheet(id: string) {
if (confirm("Are you sure you want to delete this Timesheet?")) {
@@ -862,7 +1001,7 @@
<!-- Export Button -->
<button
class="bg-green-600 text-white px-3 py-1 rounded hover:bg-green-700 text-sm"
on:click={exportTimesheets}
on:click={exportToExcelFromFrontend}
>
⬇️ Export
</button>

View File

@@ -332,6 +332,11 @@ acorn@^8.12.1, acorn@^8.14.1, acorn@^8.9.0:
resolved "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz"
integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==
adler-32@~1.3.0:
version "1.3.1"
resolved "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz"
integrity sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==
aria-query@^5.3.1:
version "5.3.2"
resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz"
@@ -342,6 +347,14 @@ axobject-query@^4.1.0:
resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz"
integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==
cfb@~1.2.1:
version "1.2.2"
resolved "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz"
integrity sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==
dependencies:
adler-32 "~1.3.0"
crc-32 "~1.2.0"
chokidar@^4.0.1:
version "4.0.3"
resolved "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz"
@@ -359,6 +372,11 @@ clsx@^2.1.1:
resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
codepage@~1.15.0:
version "1.15.0"
resolved "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz"
integrity sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==
commondir@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz"
@@ -374,6 +392,11 @@ cookie@^1.0.1:
resolved "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz"
integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
crc-32@~1.2.0, crc-32@~1.2.1:
version "1.2.2"
resolved "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz"
integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==
debug@^4.3.7, debug@^4.4.0:
version "4.4.1"
resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz"
@@ -457,6 +480,11 @@ fdir@^6.2.0, fdir@^6.4.4:
resolved "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz"
integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==
frac@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz"
integrity sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==
function-bind@^1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@@ -675,6 +703,13 @@ source-map-js@^1.2.1:
resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
ssf@~0.11.2:
version "0.11.2"
resolved "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz"
integrity sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==
dependencies:
frac "~1.1.2"
supports-preserve-symlinks-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
@@ -818,11 +853,34 @@ whatwg-url@^5.0.0:
tr46 "~0.0.3"
webidl-conversions "^3.0.0"
wmf@~1.0.1:
version "1.0.2"
resolved "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz"
integrity sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==
word@~0.3.0:
version "0.3.0"
resolved "https://registry.npmjs.org/word/-/word-0.3.0.tgz"
integrity sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==
ws@^8.18.0:
version "8.18.2"
resolved "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz"
integrity sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==
xlsx@^0.18.5:
version "0.18.5"
resolved "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz"
integrity sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==
dependencies:
adler-32 "~1.3.0"
cfb "~1.2.1"
codepage "~1.15.0"
crc-32 "~1.2.1"
ssf "~0.11.2"
wmf "~1.0.1"
word "~0.3.0"
yallist@^5.0.0:
version "5.0.0"
resolved "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz"