export file frontend
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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];
|
||||
|
||||
// B–H 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>
|
||||
|
||||
58
yarn.lock
58
yarn.lock
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user