From c8eab1f0f666a93a132309234ce48696bee8aa9b Mon Sep 17 00:00:00 2001 From: arteons Date: Wed, 23 Jul 2025 00:28:00 +0800 Subject: [PATCH] export file frontend --- package.json | 3 +- src/routes/backoffice/timesheets/+page.svelte | 201 +++++++++++++++--- yarn.lock | 58 +++++ 3 files changed, 230 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index c109ccd..cbf9d2a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/routes/backoffice/timesheets/+page.svelte b/src/routes/backoffice/timesheets/+page.svelte index 82cff4b..a6d01b0 100644 --- a/src/routes/backoffice/timesheets/+page.svelte +++ b/src/routes/backoffice/timesheets/+page.svelte @@ -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 @@ diff --git a/yarn.lock b/yarn.lock index a65c4bb..c798851 100644 --- a/yarn.lock +++ b/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"