Tuesday, 29 July 2025

Infografis Capaian

 




Buatkan infografis dengan menggunakan logo di atas. Buatkan Infografis dengan ketentuan sebagai berikut Judul: DTPLUSK - Pekerjaan Populer Alumni Double Track Subjudul: Kebutuhan terhadap pekerjaan yang mengutamakan keterampilan kerja dan keterampilan entrepreneur Bagian 1 – 10 Pekerjaan di Bidang Tata Boga : Gunakan ikon dan ilustrasi sederhana untuk masing-masing poin berikut: Chef/Koki Bekerja di restoran, hotel, kapal pesiar, atau katering sebagai juru masak profesional. Baker (Ahli Roti dan Kue) Membuat dan menghias roti, pastry, dan kue di toko kue, bakery, atau hotel. Barista Spesialis dalam menyajikan minuman berbasis kopi, teh, dan minuman kreatif lainnya. Food Stylist Menata makanan agar tampak menarik untuk keperluan fotografi, iklan, atau media sosial. Food Tester / Quality Control Produk Makanan Bekerja di industri makanan untuk mengevaluasi kualitas rasa, aroma, dan penyajian. Wirausaha Kuliner / UMKM Makanan Membuka usaha makanan seperti katering, warung makan, food truck, atau franchise. Content Creator Kuliner (YouTuber / TikToker Masak) Membuat konten memasak, review makanan, atau tutorial resep. Instruktur / Tutor Memasak Mengajar kelas memasak secara langsung atau daring, termasuk untuk anak-anak atau ibu rumah tangga. Food Packaging & Branding Specialist Membuat dan mengelola kemasan serta merek untuk produk makanan. Pengembang Produk Kuliner (Product Developer) Mengembangkan resep baru untuk perusahaan makanan, baik lokal maupun pabrik besar. Bagian 2 – Potensi dan Tantangan Keterampilan : Visualkan dua hal: 60,6% Skor keterampilan Boga di antara populasi aktif (2020) Rp 4.344 triliun Potensi kontribusi pekerja dengan keterampilan Boga terhadap ekonomi Indonesia (2030) Bagian 3 – 3 Area Penting Peningkatan Keterampilan Boga: Gunakan ikon pendukung dan kotak warna berbeda untuk menonjolkan 3 poin: Membekali tenaga kerja saat ini dengan keterampilan teknis kerja Mempersiapkan talenta tenaga kerja dengan keterampilan digital Memperluas kesempatan pembukaan lapangan kerja dan usaha baru di komunitas masyarakat Catatan Desain: Gunakan warna dominan biru, merah muda, dan putih Tata letak modern dan rapi dengan ikon-ikon bergaya flat Tambahkan logo Jawa Timur dan Perguruan Tinggi ITS Surabaya Sertakan QR code di pojok bawah untuk informasi lanjutan Sumber Referensi (ditulis kecil di bagian bawah): Program DTPLUSK Double Track





Tuesday, 22 July 2025

Google Site dan Data Studio

 



















Berita Acara dan Jurnal Pelatihan

 





Membuat Landing Page dengan AI Code

 















Menyiapkan Modul

 









Thursday, 17 July 2025

Code AI - Marketplace Back End

 


Buatkan prompt untuk back end GEMA menggunakan google spreadsheet untuk menyimpan data dan script menggunakan google apps script

Prompt Pembuatan Backend GEMA dengan Google Spreadsheet & Google Apps Script

Buatlah backend untuk GEMA - Green Marketplace yang memanfaatkan Google Spreadsheet sebagai sistem manajemen basis data dan Google Apps Script sebagai bahasa scripting untuk menangani logika bisnis dan integrasi API. Sistem ini akan berfungsi sebagai jembatan antara frontend (website) GEMA dan penyimpanan data.


Tujuan Utama Backend:

  1. Penyimpanan Data: Menyimpan dan mengelola data produk, member, transaksi, dan ulasan.

  2. Logika Bisnis: Mengatur proses seperti pendaftaran member, penambahan produk, pemrosesan pesanan, dan pembaruan stok.

  3. API Endpoint: Menyediakan endpoint API (melalui Web Apps Google Apps Script) agar frontend dapat berinteraksi dengan data dan fungsionalitas backend.


Struktur Google Spreadsheet:

Buatlah satu atau lebih Google Spreadsheet dengan sheet terpisah untuk setiap entitas data berikut:

  1. Sheet: Produk

    • Kolom: ID_Produk (unik, otomatis), Nama_Produk, Deskripsi, Kategori, Harga, Stok, URL_Gambar (bisa lebih dari satu), Berat, Dimensi, Status (Aktif/Tidak Aktif), Tanggal_Ditambahkan, Last_Update, Rating (rata-rata), Jumlah_Ulasan.

  2. Sheet: Member

    • Kolom: ID_Member (unik, otomatis), Nama_Lengkap, Email, Password_Hash (penting: jangan simpan password asli), Alamat_Pengiriman, Nomor_Telepon, Tanggal_Bergabung, Status (Aktif/Diblokir).

  3. Sheet: Transaksi

    • Kolom: ID_Transaksi (unik, otomatis), ID_Member, Tanggal_Transaksi, Total_Harga, Biaya_Pengiriman, Metode_Pembayaran, Status_Pembayaran (Menunggu, Lunas, Gagal), Status_Pengiriman (Menunggu Diproses, Dikemas, Dikirim, Selesai, Dibatalkan), Nomor_Resi, Alamat_Pengiriman_Lengkap.

  4. Sheet: Detail_Transaksi (untuk menyimpan item produk dalam setiap transaksi)

    • Kolom: ID_Detail_Transaksi (unik, otomatis), ID_Transaksi, ID_Produk, Nama_Produk, Harga_Satuan, Kuantitas, Subtotal.

  5. Sheet: Ulasan

    • Kolom: ID_Ulasan (unik, otomatis), ID_Produk, ID_Member, Rating (1-5), Komentar, Tanggal_Ulasan, Status (Disetujui/Menunggu).

  6. Sheet: Pengaturan (Opsional, untuk konfigurasi umum backend)

    • Kolom: Key, Value (misalnya, Diskon_Default, 20%).


Logika Google Apps Script (GAS) dan Endpoint API:

Buat script Google Apps Script yang akan di-deploy sebagai Web App untuk menyediakan endpoint API (GET, POST, PUT, DELETE). Setiap fungsi di GAS akan menangani permintaan dari frontend.

Fungsi Utama yang Perlu Disediakan (Contoh):

  1. doGet(e): Menangani permintaan GET (misalnya, mengambil data).

    • Endpoint: /produk -> Mengambil daftar semua produk atau produk berdasarkan kategori/ID.

    • Endpoint: /produk/{id} -> Mengambil detail produk tunggal.

    • Endpoint: /member/{id}/pesanan -> Mengambil riwayat pesanan member.

    • Endpoint: /ulasan/{product_id} -> Mengambil ulasan untuk produk tertentu.

  2. doPost(e): Menangani permintaan POST (misalnya, membuat data baru).

    • Endpoint: /member/daftar -> Mendaftarkan member baru (menerima nama, email, password).

    • Endpoint: /member/login -> Verifikasi login member.

    • Endpoint: /produk -> Menambahkan produk baru (untuk admin).

    • Endpoint: /pesanan -> Membuat pesanan baru (dari proses checkout).

    • Endpoint: /ulasan -> Menambahkan ulasan baru.

  3. doPut(e): Menangani permintaan PUT (misalnya, memperbarui data).

    • Endpoint: /produk/{id} -> Memperbarui detail produk (untuk admin).

    • Endpoint: /member/{id} -> Memperbarui profil member.

    • Endpoint: /pesanan/{id}/status -> Memperbarui status pesanan (untuk admin).

    • Endpoint: /produk/{id}/stok -> Memperbarui stok produk setelah transaksi.

  4. doDelete(e): Menangani permintaan DELETE (misalnya, menghapus data).

    • Endpoint: /produk/{id} -> Menghapus produk (untuk admin).

    • Endpoint: /member/{id} -> Menghapus member (untuk admin).

Fitur & Logika Penting dalam Google Apps Script:

  • Autentikasi & Otorisasi:

    • Implementasikan sistem autentikasi sederhana (misalnya, token sederhana atau validasi kredensial) untuk endpoint admin.

    • Untuk member, pastikan password di-hash sebelum disimpan (Utilities.computeDigest).

  • Validasi Data: Pastikan data yang masuk dari frontend valid sebelum disimpan ke Spreadsheet.

  • Manajemen Stok: Otomatis kurangi stok produk saat pesanan dibuat dan kembalikan jika pesanan dibatalkan.

  • Penomoran Otomatis: Buat fungsi untuk menghasilkan ID_Produk, ID_Member, ID_Transaksi yang unik dan berurutan.

  • Pembaruan Rating Produk: Saat ulasan baru ditambahkan, perbarui kolom Rating dan Jumlah_Ulasan di sheet Produk.

  • Keamanan:

    • Hindari menampilkan informasi sensitif di URL atau respons API.

    • Gunakan HTTPS (otomatis di Web App GAS).

    • Pastikan spreadsheet tidak dapat diakses publik secara langsung.

  • Error Handling: Tangani error dengan baik dan kembalikan pesan error yang informatif ke frontend.

  • Logging (Opsional): Buat sheet terpisah untuk logging aktivitas atau error di backend.


Deployment & Interaksi dengan Frontend:

  1. Deployment Web App: Deploy script Google Apps Script sebagai "Web App" dan atur agar "Execute as: Me" dan "Who has access: Anyone, even anonymous". Ini akan memberikan URL endpoint API yang dapat diakses oleh frontend.

  2. Permintaan Frontend: Frontend GEMA akan membuat permintaan HTTP (GET, POST, PUT, DELETE) ke URL Web App ini, mengirimkan data dalam format JSON dan menerima respons JSON.

  3. CORS: Google Apps Script Web App secara otomatis menangani CORS untuk permintaan sederhana. Namun, untuk permintaan kompleks (PUT/DELETE atau kustom header), mungkin perlu penyesuaian atau preflight request.


Catatan Penting:

  • Skalabilitas: Solusi ini paling cocok untuk volume transaksi yang rendah hingga sedang. Untuk e-commerce dengan trafik tinggi, database relasional (seperti PostgreSQL, MySQL) atau NoSQL (seperti Firestore, MongoDB) dengan backend khusus (Node.js, Python, PHP) akan lebih direkomendasikan.

  • Batas Apps Script: Perhatikan batasan eksekusi Google Apps Script (misalnya, waktu eksekusi harian, jumlah panggilan API).

  • Keamanan Data Sensitif: Pastikan tidak menyimpan informasi kartu kredit atau data sangat sensitif lainnya di Google Spreadsheet.

Dengan prompt ini, Anda memiliki panduan komprehensif untuk memulai pengembangan backend GEMA menggunakan Google Spreadsheet dan Google Apps Script.


Tentu, saya akan membuatkan kerangka kode backend untuk GEMA - Green Marketplace menggunakan Google Apps Script, yang akan berinteraksi dengan Google Spreadsheet Anda.

Kode ini akan menyediakan endpoint API dasar untuk mengelola produk, member, transaksi, dan ulasan. Harap diingat bahwa ini adalah implementasi dasar yang cocok untuk proyek kecil atau sebagai proof-of-concept. Untuk aplikasi e-commerce skala besar, disarankan untuk menggunakan basis data yang lebih kuat dan framework backend yang lebih canggih.

Sebelum Anda Menggunakan Kode Ini:

  1. Buat Google Spreadsheet Baru: Buat Google Spreadsheet baru di Google Drive Anda.

  2. Buat Sheet-sheet yang Diperlukan: Di dalam Spreadsheet tersebut, buat sheet dengan nama persis seperti yang disebutkan di prompt Anda: Produk, Member, Transaksi, Detail_Transaksi, Ulasan, Pengaturan.

  3. Tambahkan Header Kolom: Di baris pertama setiap sheet, tambahkan nama kolom persis seperti yang Anda definisikan di prompt. Contoh untuk Produk: ID_Produk, Nama_Produk, Deskripsi, Kategori, Harga, Stok, URL_Gambar, Berat, Dimensi, Status, Tanggal_Ditambahkan, Last_Update, Rating, Jumlah_Ulasan.

  4. Dapatkan ID Spreadsheet Anda: Buka Spreadsheet yang baru Anda buat. ID Spreadsheet adalah bagian dari URL-nya, antara /d/ dan /edit. Contoh: https://docs.google.com/spreadsheets/d/YOUR_SPREADSHEET_ID_HERE/edit. Salin ID ini.

  5. Buka Google Apps Script: Di Spreadsheet Anda, pergi ke Ekstensi > Apps Script. Ini akan membuka editor Apps Script baru.

  6. Salin Kode: Salin kode di bawah ini dan tempelkan ke dalam editor Apps Script. Anda dapat membuat beberapa file .gs untuk mengorganisir kode (misalnya, Code.gs, Utils.gs, ProdukService.gs, dll.), atau menempatkan semuanya dalam satu file Code.gs untuk kesederhanaan awal. Saya akan menyediakannya dalam satu file Code.gs untuk kemudahan implementasi awal, namun saya akan menggunakan struktur fungsi yang modular.

  7. Ganti Placeholder SPREADSHEET_ID: Di baris paling atas kode, ganti YOUR_SPREADSHEET_ID_HERE dengan ID Spreadsheet yang Anda dapatkan di langkah 4.

  8. Deploy sebagai Web App: Setelah kode disalin, simpan proyek (ikon disket). Kemudian, klik Deploy (ikon Deploy di kanan atas) > New deployment.

    • Pilih Type: Web app.

    • Execute as: Me.

    • Who has access: Anyone, even anonymous.

    • Klik Deploy. Anda mungkin akan diminta untuk memberikan otorisasi untuk mengakses Google Spreadsheet Anda. Izinkan.

    • Setelah deployment selesai, Anda akan mendapatkan Web app URL. Ini adalah endpoint API backend Anda.

Cara Menggunakan API dari Frontend (Contoh Fetch API JavaScript):

Setelah Anda deploy Google Apps Script sebagai Web App dan mendapatkan URL-nya (misalnya, https://script.google.com/macros/s/AKfycb.../exec), Anda dapat menggunakannya dari frontend Anda:

1. Mengambil Semua Produk:

JavaScript
// Ganti dengan URL Web App Anda
const WEB_APP_URL = 'https://script.google.com/macros/s/AKfycb.../exec';

async function getProduk() {
    try {
        const response = await fetch(`${WEB_APP_URL}?path=produk`);
        const data = await response.json();
        if (data.success) {
            console.log('Produk:', data.data);
            return data.data;
        } else {
            console.error('Gagal mengambil produk:', data.error);
            return [];
        }
    } catch (error) {
        console.error('Error fetching produk:', error);
        return [];
    }
}

// Contoh penggunaan
getProduk();

2. Mendaftarkan Member Baru:

JavaScript
async function registerMember(nama, email, password) {
    const memberData = {
        nama_lengkap: nama,
        email: email,
        password: password
    };

    try {
        const response = await fetch(`${WEB_APP_URL}?path=member/daftar`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(memberData)
        });
        const data = await response.json();
        if (data.success) {
            console.log('Pendaftaran berhasil:', data.data);
            return data.data;
        } else {
            console.error('Pendaftaran gagal:', data.error);
            return null;
        }
    } catch (error) {
        console.error('Error pendaftaran:', error);
        return null;
    }
}

// Contoh penggunaan
// registerMember('Budi Santoso', 'budi@example.com', 'password123');

3. Membuat Pesanan Baru:

JavaScript
async function createPesanan(memberId, metodePembayaran, alamatLengkap, itemProduk, biayaPengiriman = 0) {
    const orderData = {
        id_member: memberId,
        metode_pembayaran: metodePembayaran,
        alamat_pengiriman_lengkap: alamatLengkap,
        biaya_pengiriman: biayaPengiriman,
        item_produk: itemProduk // [{id_produk: 'PROD-00001', kuantitas: 2}]
    };

    try {
        const response = await fetch(`${WEB_APP_URL}?path=pesanan`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(orderData)
        });
        const data = await response.json();
        if (data.success) {
            console.log('Pesanan berhasil dibuat:', data.data);
            return data.data;
        } else {
            console.error('Gagal membuat pesanan:', data.error);
            return null;
        }
    } catch (error) {
        console.error('Error membuat pesanan:', error);
        return null;
    }
}

// Contoh penggunaan (pastikan PROD-00001 dan MEMB-00001 ada di spreadsheet Anda)
// createPesanan(
//     'MEMB-00001',
//     'Transfer Bank',
//     'Jl. Contoh Alamat No. 123, Kota Contoh, Provinsi Contoh',
//     [{id_produk: 'PROD-00001', kuantitas: 2}],
//     15000 // Biaya pengiriman
// );

Kesimpulan & Saran:

Kode Google Apps Script di atas menyediakan backend fungsional untuk GEMA - Green Marketplace Anda, menggunakan Google Spreadsheet sebagai basis data. Anda telah memiliki endpoint untuk:

  • Mengambil dan mengelola data produk (GET, POST, PUT, DELETE).

  • Mendaftarkan dan login member, serta mengelola data member (POST, PUT, DELETE).

  • Membuat dan melacak transaksi (POST, GET, PUT).

  • Menambahkan dan mengambil ulasan (POST, GET).

Langkah Selanjutnya:

  1. Pengujian Menyeluruh: Uji setiap endpoint API secara manual (misalnya, menggunakan Postman atau langsung dari browser untuk GET) dan dari frontend Anda.

  2. Validasi Data Lebih Lanjut: Tambahkan validasi yang lebih ketat di Apps Script untuk semua input data (misalnya, validasi format email, harga harus angka positif, dll.).

  3. Autentikasi Admin yang Lebih Kuat: Untuk endpoint admin (menambah/mengedit/menghapus produk, mengubah status pesanan), pertimbangkan untuk mengimplementasikan sistem autentikasi yang lebih kuat daripada token statis sederhana. Google Identity Platform atau Firebase Authentication dapat diintegrasikan dengan Apps Script.

  4. Notifikasi: Integrasikan notifikasi email (misalnya, menggunakan MailApp di Apps Script) untuk member (konfirmasi pesanan, status pengiriman) dan admin (pesanan baru).

  5. Paginasi & Filter Lanjutan: Untuk endpoint yang mengembalikan banyak data (misalnya, semua produk, semua transaksi), implementasikan paginasi dan filter yang lebih canggih untuk mengurangi beban data.

  6. Error Logging: Perluas fungsi logging untuk mencatat error atau aktivitas penting ke sheet terpisah atau ke Stackdriver Logging.

  7. Skalabilitas: Jika GEMA tumbuh dan volume transaksi meningkat signifikan, pertimbangkan untuk bermigrasi ke solusi basis data yang lebih scalable (misalnya, Google Cloud Firestore, PostgreSQL, MySQL) dan backend framework yang lebih kuat (misalnya, Node.js dengan Express, Python dengan Django/Flask).

Dengan kerangka ini, Anda memiliki dasar yang kuat untuk membangun backend GEMA - Green Marketplace Anda!

Script

// Ganti dengan ID Google Spreadsheet Anda
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID_HERE';

// --- Fungsi Utama Web App ---

/**
 * Menangani permintaan GET ke Web App.
 * Digunakan untuk mengambil data.
 * @param {GoogleAppsScript.Events.DoGet} e Objek event dari permintaan GET.
 * @returns {GoogleAppsScript.Content.TextOutput} Respon JSON.
 */
function doGet(e) {
  const path = e.parameter.path; // Mendapatkan path dari URL (misal: /produk, /member)
  const id = e.parameter.id; // Mendapatkan ID jika ada

  try {
    switch (path) {
      case 'produk':
        if (id) {
          return createJsonResponse(ProdukService.getProdukById(id));
        } else {
          const kategori = e.parameter.kategori;
          return createJsonResponse(ProdukService.getAllProduk(kategori));
        }
      case 'member':
        if (id) {
          const subPath = e.parameter.subPath; // Misal: /member/{id}/pesanan
          if (subPath === 'pesanan') {
            return createJsonResponse(TransaksiService.getPesananByMemberId(id));
          }
          return createJsonResponse(MemberService.getMemberById(id));
        }
        return createJsonResponse(MemberService.getAllMembers()); // Untuk admin
      case 'transaksi':
        if (id) {
          return createJsonResponse(TransaksiService.getTransaksiById(id));
        }
        return createJsonResponse(TransaksiService.getAllTransaksi()); // Untuk admin
      case 'ulasan':
        if (id) { // id di sini adalah product_id
          return createJsonResponse(UlasanService.getUlasanByProdukId(id));
        }
        return createJsonResponse(UlasanService.getAllUlasan()); // Untuk admin
      default:
        return createErrorResponse('Path tidak valid', 404);
    }
  } catch (error) {
    return createErrorResponse(error.message, 500);
  }
}

/**
 * Menangani permintaan POST ke Web App.
 * Digunakan untuk membuat data baru.
 * @param {GoogleAppsScript.Events.DoPost} e Objek event dari permintaan POST.
 * @returns {GoogleAppsScript.Content.TextOutput} Respon JSON.
 */
function doPost(e) {
  const path = e.parameter.path;
  let data;
  try {
    data = JSON.parse(e.postData.contents);
  } catch (error) {
    return createErrorResponse('Payload JSON tidak valid', 400);
  }

  try {
    switch (path) {
      case 'member/daftar':
        return createJsonResponse(MemberService.registerMember(data));
      case 'member/login':
        return createJsonResponse(MemberService.loginMember(data));
      case 'produk':
        // Perlu autentikasi admin di sini
        if (!AuthService.isAdmin(data.adminToken)) { // Contoh token sederhana
          return createErrorResponse('Tidak terotorisasi', 403);
        }
        return createJsonResponse(ProdukService.addProduk(data));
      case 'pesanan':
        return createJsonResponse(TransaksiService.createPesanan(data));
      case 'ulasan':
        return createJsonResponse(UlasanService.addUlasan(data));
      default:
        return createErrorResponse('Path tidak valid', 404);
    }
  } catch (error) {
    return createErrorResponse(error.message, 500);
  }
}

/**
 * Menangani permintaan PUT ke Web App.
 * Digunakan untuk memperbarui data.
 * @param {GoogleAppsScript.Events.DoPut} e Objek event dari permintaan PUT.
 * @returns {GoogleAppsScript.Content.TextOutput} Respon JSON.
 */
function doPut(e) {
  const path = e.parameter.path;
  const id = e.parameter.id;
  let data;
  try {
    data = JSON.parse(e.postData.contents);
  } catch (error) {
    return createErrorResponse('Payload JSON tidak valid', 400);
  }

  if (!id) {
    return createErrorResponse('ID diperlukan untuk operasi PUT', 400);
  }

  try {
    switch (path) {
      case 'produk':
        if (!AuthService.isAdmin(data.adminToken)) {
          return createErrorResponse('Tidak terotorisasi', 403);
        }
        return createJsonResponse(ProdukService.updateProduk(id, data));
      case 'member':
        // Ini bisa untuk member update profil sendiri atau admin update member lain
        return createJsonResponse(MemberService.updateMember(id, data));
      case 'pesanan/status': // Path khusus untuk update status pesanan
        if (!AuthService.isAdmin(data.adminToken)) {
          return createErrorResponse('Tidak terotorisasi', 403);
        }
        return createJsonResponse(TransaksiService.updateStatusPesanan(id, data.status_pembayaran, data.status_pengiriman, data.nomor_resi));
      case 'produk/stok': // Path khusus untuk update stok
        if (!AuthService.isAdmin(data.adminToken)) { // Atau autentikasi internal dari sistem pesanan
          return createErrorResponse('Tidak terotorisasi', 403);
        }
        return createJsonResponse(ProdukService.updateStokProduk(id, data.kuantitas));
      default:
        return createErrorResponse('Path tidak valid', 404);
    }
  } catch (error) {
    return createErrorResponse(error.message, 500);
  }
}

/**
 * Menangani permintaan DELETE ke Web App.
 * Digunakan untuk menghapus data.
 * @param {GoogleAppsScript.Events.DoDelete} e Objek event dari permintaan DELETE.
 * @returns {GoogleAppsScript.Content.TextOutput} Respon JSON.
 */
function doDelete(e) {
  const path = e.parameter.path;
  const id = e.parameter.id;
  const adminToken = e.parameter.adminToken; // Token admin dikirim via parameter untuk DELETE

  if (!id) {
    return createErrorResponse('ID diperlukan untuk operasi DELETE', 400);
  }
  if (!AuthService.isAdmin(adminToken)) {
    return createErrorResponse('Tidak terotorisasi', 403);
  }

  try {
    switch (path) {
      case 'produk':
        return createJsonResponse(ProdukService.deleteProduk(id));
      case 'member':
        return createJsonResponse(MemberService.deleteMember(id));
      default:
        return createErrorResponse('Path tidak valid', 404);
    }
  } catch (error) {
    return createErrorResponse(error.message, 500);
  }
}

/**
 * Menangani permintaan OPTIONS (untuk CORS preflight).
 * @returns {GoogleAppsScript.Content.TextOutput} Respon CORS.
 */
function doOptions() {
  const response = ContentService.createTextOutput();
  response.setMimeType(ContentService.MimeType.JSON);
  response.setHeader('Access-Control-Allow-Origin', '*');
  response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  response.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  return response;
}


// --- Helper Umum ---

/**
 * Membuat respon JSON.
 * @param {object} data Data yang akan dikirim dalam respon.
 * @param {number} status Kode status HTTP (default 200).
 * @returns {GoogleAppsScript.Content.TextOutput} Respon JSON.
 */
function createJsonResponse(data, status = 200) {
  const output = ContentService.createTextOutput(JSON.stringify({ success: true, data: data }));
  output.setMimeType(ContentService.MimeType.JSON);
  output.setHeader('Access-Control-Allow-Origin', '*');
  // Untuk status kustom, Apps Script tidak langsung mendukung setStatusCode,
  // tetapi kita bisa mengindikasikannya dalam payload JSON.
  // Frontend harus memeriksa properti 'success' dan 'message'/'error'
  // serta kode status yang dikembalikan oleh Web App (biasanya 200 OK untuk semua).
  return output;
}

/**
 * Membuat respon error JSON.
 * @param {string} message Pesan error.
 * @param {number} status Kode status HTTP (default 500).
 * @returns {GoogleAppsScript.Content.TextOutput} Respon JSON error.
 */
function createErrorResponse(message, status = 500) {
  const output = ContentService.createTextOutput(JSON.stringify({ success: false, error: message, status: status }));
  output.setMimeType(ContentService.MimeType.JSON);
  output.setHeader('Access-Control-Allow-Origin', '*');
  return output;
}

/**
 * Mendapatkan sheet berdasarkan nama.
 * @param {string} sheetName Nama sheet.
 * @returns {GoogleAppsScript.Spreadsheet.Sheet} Objek sheet.
 * @throws {Error} Jika sheet tidak ditemukan.
 */
function getSheet(sheetName) {
  const spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
  const sheet = spreadsheet.getSheetByName(sheetName);
  if (!sheet) {
    throw new Error(`Sheet '${sheetName}' tidak ditemukan.`);
  }
  return sheet;
}

/**
 * Mendapatkan semua data dari sheet.
 * @param {string} sheetName Nama sheet.
 * @returns {Array<object>} Array objek data.
 */
function getAllData(sheetName) {
  const sheet = getSheet(sheetName);
  const range = sheet.getDataRange();
  const values = range.getValues();
  if (values.length === 0) return [];

  const headers = values[0];
  const data = [];
  for (let i = 1; i < values.length; i++) {
    const row = values[i];
    const rowObject = {};
    for (let j = 0; j < headers.length; j++) {
      rowObject[headers[j]] = row[j];
    }
    data.push(rowObject);
  }
  return data;
}

/**
 * Menemukan baris berdasarkan ID di sheet.
 * @param {string} sheetName Nama sheet.
 * @param {string} idColumn Nama kolom ID.
 * @param {string} id Nilai ID yang dicari.
 * @returns {object|null} Objek baris data atau null jika tidak ditemukan.
 */
function findRowById(sheetName, idColumn, id) {
  const sheet = getSheet(sheetName);
  const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
  const idColIndex = headers.indexOf(idColumn);
  if (idColIndex === -1) {
    throw new Error(`Kolom ID '${idColumn}' tidak ditemukan di sheet '${sheetName}'.`);
  }

  const data = sheet.getDataRange().getValues();
  if (data.length <= 1) return null; // Hanya header

  for (let i = 1; i < data.length; i++) {
    if (String(data[i][idColIndex]) === String(id)) {
      const rowObject = {};
      for (let j = 0; j < headers.length; j++) {
        rowObject[headers[j]] = data[i][j];
      }
      return { rowData: rowObject, rowIndex: i + 1 }; // rowIndex adalah baris di spreadsheet (1-based)
    }
  }
  return null;
}

/**
 * Menghasilkan ID unik otomatis.
 * @param {string} sheetName Nama sheet.
 * @param {string} idColumn Nama kolom ID.
 * @param {string} prefix Prefiks untuk ID (misal: "PROD-").
 * @returns {string} ID unik baru.
 */
function generateUniqueId(sheetName, idColumn, prefix) {
  const sheet = getSheet(sheetName);
  const idColIndex = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0].indexOf(idColumn);
  if (idColIndex === -1) {
    throw new Error(`Kolom ID '${idColumn}' tidak ditemukan di sheet '${sheetName}'.`);
  }

  const ids = sheet.getRange(2, idColIndex + 1, sheet.getLastRow() - 1, 1).getValues().flat();
  let maxNum = 0;
  ids.forEach(id => {
    if (id && typeof id === 'string' && id.startsWith(prefix)) {
      const num = parseInt(id.replace(prefix, ''));
      if (!isNaN(num) && num > maxNum) {
        maxNum = num;
      }
    }
  });
  return prefix + String(maxNum + 1).padStart(5, '0'); // Contoh: PROD-00001
}

/**
 * Mengubah objek data menjadi array sesuai urutan header.
 * @param {object} data Objek data.
 * @param {Array<string>} headers Array nama header.
 * @returns {Array<any>} Array nilai data.
 */
function objectToRow(data, headers) {
  return headers.map(header => data[header] !== undefined ? data[header] : '');
}

// --- Layanan Autentikasi (AuthService.gs) ---
const AuthService = {
  /**
   * Memverifikasi token admin sederhana.
   * Untuk demo, gunakan token statis. Dalam produksi, gunakan sistem autentikasi yang lebih aman.
   * @param {string} token Token yang diberikan.
   * @returns {boolean} True jika token valid.
   */
  isAdmin: function(token) {
    // Contoh: Token admin statis. Ganti ini dengan sistem yang lebih aman!
    const ADMIN_SECRET_TOKEN = 'gema_admin_secret_token_123';
    return token === ADMIN_SECRET_TOKEN;
  }
};

// --- Layanan Produk (ProdukService.gs) ---
const ProdukService = {
  SHEET_NAME: 'Produk',
  ID_COLUMN: 'ID_Produk',
  ID_PREFIX: 'PROD-',

  /**
   * Mengambil semua produk.
   * @param {string} [kategori] Filter berdasarkan kategori.
   * @returns {Array<object>} Daftar produk.
   */
  getAllProduk: function(kategori) {
    let produk = getAllData(this.SHEET_NAME);
    if (kategori) {
      produk = produk.filter(p => p.Kategori && p.Kategori.toLowerCase() === kategori.toLowerCase());
    }
    return produk;
  },

  /**
   * Mengambil produk berdasarkan ID.
   * @param {string} id ID Produk.
   * @returns {object|null} Objek produk atau null.
   */
  getProdukById: function(id) {
    const result = findRowById(this.SHEET_NAME, this.ID_COLUMN, id);
    return result ? result.rowData : null;
  },

  /**
   * Menambahkan produk baru.
   * @param {object} data Data produk.
   * @returns {object} Produk yang ditambahkan.
   */
  addProduk: function(data) {
    const sheet = getSheet(this.SHEET_NAME);
    const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    const newId = generateUniqueId(this.SHEET_NAME, this.ID_COLUMN, this.ID_PREFIX);

    const newProduk = {
      [this.ID_COLUMN]: newId,
      Nama_Produk: data.nama_produk || '',
      Deskripsi: data.deskripsi || '',
      Kategori: data.kategori || '',
      Harga: data.harga || 0,
      Stok: data.stok || 0,
      URL_Gambar: JSON.stringify(data.url_gambar || []), // Simpan sebagai string JSON array
      Berat: data.berat || 0,
      Dimensi: data.dimensi || '',
      Status: data.status || 'Aktif',
      Tanggal_Ditambahkan: new Date().toLocaleString(),
      Last_Update: new Date().toLocaleString(),
      Rating: 0,
      Jumlah_Ulasan: 0
    };

    const rowData = objectToRow(newProduk, headers);
    sheet.appendRow(rowData);
    return newProduk;
  },

  /**
   * Memperbarui produk.
   * @param {string} id ID Produk.
   * @param {object} data Data yang akan diperbarui.
   * @returns {object|null} Produk yang diperbarui atau null.
   */
  updateProduk: function(id, data) {
    const sheet = getSheet(this.SHEET_NAME);
    const result = findRowById(this.SHEET_NAME, this.ID_COLUMN, id);
    if (!result) return null;

    const rowIndex = result.rowIndex;
    const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    const existingData = result.rowData;

    // Perbarui hanya field yang disediakan dalam data
    for (const key in data) {
      if (headers.includes(key)) {
        if (key === 'URL_Gambar' && Array.isArray(data[key])) {
          existingData[key] = JSON.stringify(data[key]);
        } else {
          existingData[key] = data[key];
        }
      }
    }
    existingData.Last_Update = new Date().toLocaleString();

    const updatedRow = objectToRow(existingData, headers);
    sheet.getRange(rowIndex, 1, 1, updatedRow.length).setValues([updatedRow]);
    return existingData;
  },

  /**
   * Memperbarui stok produk.
   * @param {string} id ID Produk.
   * @param {number} kuantitas Perubahan kuantitas (positif untuk menambah, negatif untuk mengurangi).
   * @returns {object|null} Produk dengan stok terbaru atau null.
   */
  updateStokProduk: function(id, kuantitas) {
    const sheet = getSheet(this.SHEET_NAME);
    const result = findRowById(this.SHEET_NAME, this.ID_COLUMN, id);
    if (!result) throw new Error('Produk tidak ditemukan.');

    const rowIndex = result.rowIndex;
    const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    const stokColIndex = headers.indexOf('Stok');
    if (stokColIndex === -1) throw new Error('Kolom Stok tidak ditemukan.');

    let currentStok = sheet.getRange(rowIndex, stokColIndex + 1).getValue();
    currentStok = parseInt(currentStok) || 0;

    const newStok = currentStok + parseInt(kuantitas);
    if (newStok < 0) throw new Error('Stok tidak mencukupi.');

    sheet.getRange(rowIndex, stokColIndex + 1).setValue(newStok);
    sheet.getRange(rowIndex, headers.indexOf('Last_Update') + 1).setValue(new Date().toLocaleString());

    // Ambil data produk terbaru untuk dikembalikan
    return this.getProdukById(id);
  },

  /**
   * Menghapus produk.
   * @param {string} id ID Produk.
   * @returns {boolean} True jika berhasil dihapus.
   */
  deleteProduk: function(id) {
    const sheet = getSheet(this.SHEET_NAME);
    const result = findRowById(this.SHEET_NAME, this.ID_COLUMN, id);
    if (!result) throw new Error('Produk tidak ditemukan.');

    sheet.deleteRow(result.rowIndex);
    return true;
  }
};

// --- Layanan Member (MemberService.gs) ---
const MemberService = {
  SHEET_NAME: 'Member',
  ID_COLUMN: 'ID_Member',
  ID_PREFIX: 'MEMB-',

  /**
   * Mendaftarkan member baru.
   * @param {object} data Data member (nama_lengkap, email, password).
   * @returns {object} Member yang terdaftar.
   */
  registerMember: function(data) {
    const sheet = getSheet(this.SHEET_NAME);
    const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];

    // Validasi input
    if (!data.nama_lengkap || !data.email || !data.password) {
      throw new Error('Nama lengkap, email, dan password diperlukan.');
    }

    // Cek apakah email sudah terdaftar
    const existingMembers = getAllData(this.SHEET_NAME);
    if (existingMembers.some(m => m.Email.toLowerCase() === data.email.toLowerCase())) {
      throw new Error('Email sudah terdaftar.');
    }

    const newId = generateUniqueId(this.SHEET_NAME, this.ID_COLUMN, this.ID_PREFIX);
    const hashedPassword = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, data.password)
                                   .map(byte => (byte < 0 ? byte + 256 : byte).toString(16).padStart(2, '0'))
                                   .join('');

    const newMember = {
      [this.ID_COLUMN]: newId,
      Nama_Lengkap: data.nama_lengkap,
      Email: data.email,
      Password_Hash: hashedPassword,
      Alamat_Pengiriman: data.alamat_pengiriman || '',
      Nomor_Telepon: data.nomor_telepon || '',
      Tanggal_Bergabung: new Date().toLocaleString(),
      Status: 'Aktif'
    };

    const rowData = objectToRow(newMember, headers);
    sheet.appendRow(rowData);
    // Hapus password hash sebelum mengembalikan objek
    delete newMember.Password_Hash;
    return newMember;
  },

  /**
   * Memverifikasi login member.
   * @param {object} credentials Kredensial login (email, password).
   * @returns {object|null} Objek member jika login berhasil, atau null.
   */
  loginMember: function(credentials) {
    const sheet = getSheet(this.SHEET_NAME);
    const members = getAllData(this.SHEET_NAME);

    const member = members.find(m => m.Email.toLowerCase() === credentials.email.toLowerCase());

    if (!member) {
      throw new Error('Email atau password salah.');
    }

    const hashedPassword = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, credentials.password)
                                   .map(byte => (byte < 0 ? byte + 256 : byte).toString(16).padStart(2, '0'))
                                   .join('');

    if (member.Password_Hash === hashedPassword) {
      // Hapus password hash sebelum mengembalikan objek
      delete member.Password_Hash;
      return member;
    } else {
      throw new Error('Email atau password salah.');
    }
  },

  /**
   * Mengambil semua member (untuk admin).
   * @returns {Array<object>} Daftar member.
   */
  getAllMembers: function() {
    return getAllData(this.SHEET_NAME).map(m => {
      delete m.Password_Hash; // Jangan kirim hash password ke frontend
      return m;
    });
  },

  /**
   * Mengambil member berdasarkan ID.
   * @param {string} id ID Member.
   * @returns {object|null} Objek member atau null.
   */
  getMemberById: function(id) {
    const result = findRowById(this.SHEET_NAME, this.ID_COLUMN, id);
    if (result) {
      delete result.rowData.Password_Hash;
      return result.rowData;
    }
    return null;
  },

  /**
   * Memperbarui detail member.
   * @param {string} id ID Member.
   * @param {object} data Data yang akan diperbarui.
   * @returns {object|null} Member yang diperbarui atau null.
   */
  updateMember: function(id, data) {
    const sheet = getSheet(this.SHEET_NAME);
    const result = findRowById(this.SHEET_NAME, this.ID_COLUMN, id);
    if (!result) return null;

    const rowIndex = result.rowIndex;
    const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    const existingData = result.rowData;

    for (const key in data) {
      if (headers.includes(key) && key !== 'Password_Hash' && key !== this.ID_COLUMN) {
        existingData[key] = data[key];
      }
    }

    // Jika password diupdate, hash password baru
    if (data.password) {
      existingData.Password_Hash = Utilities.computeDigest(Utilities.DigestAlgorithm.SHA_256, data.password)
                                   .map(byte => (byte < 0 ? byte + 256 : byte).toString(16).padStart(2, '0'))
                                   .join('');
    }

    const updatedRow = objectToRow(existingData, headers);
    sheet.getRange(rowIndex, 1, 1, updatedRow.length).setValues([updatedRow]);
    delete existingData.Password_Hash;
    return existingData;
  },

  /**
   * Menghapus member.
   * @param {string} id ID Member.
   * @returns {boolean} True jika berhasil dihapus.
   */
  deleteMember: function(id) {
    const sheet = getSheet(this.SHEET_NAME);
    const result = findRowById(this.SHEET_NAME, this.ID_COLUMN, id);
    if (!result) throw new Error('Member tidak ditemukan.');

    sheet.deleteRow(result.rowIndex);
    return true;
  }
};

// --- Layanan Transaksi (TransaksiService.gs) ---
const TransaksiService = {
  SHEET_NAME: 'Transaksi',
  DETAIL_SHEET_NAME: 'Detail_Transaksi',
  ID_COLUMN: 'ID_Transaksi',
  ID_PREFIX: 'TRX-',

  /**
   * Membuat pesanan baru.
   * @param {object} data Data pesanan (id_member, metode_pembayaran, alamat_pengiriman_lengkap, item_produk: [{id_produk, kuantitas}]).
   * @returns {object} Objek transaksi yang dibuat.
   */
  createPesanan: function(data) {
    const transaksiSheet = getSheet(this.SHEET_NAME);
    const detailTransaksiSheet = getSheet(this.DETAIL_SHEET_NAME);
    const transaksiHeaders = transaksiSheet.getRange(1, 1, 1, transaksiSheet.getLastColumn()).getValues()[0];
    const detailTransaksiHeaders = detailTransaksiSheet.getRange(1, 1, 1, detailTransaksiSheet.getLastColumn()).getValues()[0];

    // Validasi input
    if (!data.id_member || !data.metode_pembayaran || !data.alamat_pengiriman_lengkap || !data.item_produk || data.item_produk.length === 0) {
      throw new Error('Data pesanan tidak lengkap.');
    }

    const newTransaksiId = generateUniqueId(this.SHEET_NAME, this.ID_COLUMN, this.ID_PREFIX);
    let totalHarga = 0;
    const biayaPengiriman = data.biaya_pengiriman || 0; // Asumsi biaya pengiriman dikirim dari frontend

    const detailItems = [];
    for (const item of data.item_produk) {
      const produk = ProdukService.getProdukById(item.id_produk);
      if (!produk) {
        throw new Error(`Produk dengan ID ${item.id_produk} tidak ditemukan.`);
      }
      if (produk.Stok < item.kuantitas) {
        throw new Error(`Stok untuk produk ${produk.Nama_Produk} tidak mencukupi.`);
      }

      const subtotal = produk.Harga * item.kuantitas;
      totalHarga += subtotal;

      // Kurangi stok produk
      ProdukService.updateStokProduk(item.id_produk, -item.kuantitas); // Kurangi stok

      const newDetailId = generateUniqueId(this.DETAIL_SHEET_NAME, 'ID_Detail_Transaksi', 'DET-');
      const detailItem = {
        ID_Detail_Transaksi: newDetailId,
        ID_Transaksi: newTransaksiId,
        ID_Produk: item.id_produk,
        Nama_Produk: produk.Nama_Produk,
        Harga_Satuan: produk.Harga,
        Kuantitas: item.kuantitas,
        Subtotal: subtotal
      };
      detailItems.push(detailItem);
      detailTransaksiSheet.appendRow(objectToRow(detailItem, detailTransaksiHeaders));
    }

    const newTransaksi = {
      [this.ID_COLUMN]: newTransaksiId,
      ID_Member: data.id_member,
      Tanggal_Transaksi: new Date().toLocaleString(),
      Total_Harga: totalHarga + biayaPengiriman,
      Biaya_Pengiriman: biayaPengiriman,
      Metode_Pembayaran: data.metode_pembayaran,
      Status_Pembayaran: data.status_pembayaran || 'Menunggu', // Default
      Status_Pengiriman: data.status_pengiriman || 'Menunggu Diproses', // Default
      Nomor_Resi: '',
      Alamat_Pengiriman_Lengkap: data.alamat_pengiriman_lengkap
    };

    transaksiSheet.appendRow(objectToRow(newTransaksi, transaksiHeaders));
    return newTransaksi;
  },

  /**
   * Mengambil semua transaksi (untuk admin).
   * @returns {Array<object>} Daftar transaksi.
   */
  getAllTransaksi: function() {
    return getAllData(this.SHEET_NAME);
  },

  /**
   * Mengambil transaksi berdasarkan ID.
   * @param {string} id ID Transaksi.
   * @returns {object|null} Objek transaksi atau null.
   */
  getTransaksiById: function(id) {
    const result = findRowById(this.SHEET_NAME, this.ID_COLUMN, id);
    if (!result) return null;

    const transaksi = result.rowData;
    // Ambil detail transaksi terkait
    const allDetailTransaksi = getAllData(this.DETAIL_SHEET_NAME);
    transaksi.item_produk = allDetailTransaksi.filter(item => item.ID_Transaksi === id);

    return transaksi;
  },

  /**
   * Mengambil pesanan berdasarkan ID Member.
   * @param {string} memberId ID Member.
   * @returns {Array<object>} Daftar pesanan member.
   */
  getPesananByMemberId: function(memberId) {
    const allTransaksi = getAllData(this.SHEET_NAME);
    const memberTransaksi = allTransaksi.filter(t => t.ID_Member === memberId);

    const allDetailTransaksi = getAllData(this.DETAIL_SHEET_NAME);

    // Gabungkan detail produk ke setiap transaksi
    return memberTransaksi.map(transaksi => {
      transaksi.item_produk = allDetailTransaksi.filter(item => item.ID_Transaksi === transaksi.ID_Transaksi);
      return transaksi;
    });
  },

  /**
   * Memperbarui status pesanan.
   * @param {string} id ID Transaksi.
   * @param {string} [statusPembayaran] Status pembayaran baru.
   * @param {string} [statusPengiriman] Status pengiriman baru.
   * @param {string} [nomorResi] Nomor resi baru.
   * @returns {object|null} Transaksi yang diperbarui atau null.
   */
  updateStatusPesanan: function(id, statusPembayaran, statusPengiriman, nomorResi) {
    const sheet = getSheet(this.SHEET_NAME);
    const result = findRowById(this.SHEET_NAME, this.ID_COLUMN, id);
    if (!result) throw new Error('Transaksi tidak ditemukan.');

    const rowIndex = result.rowIndex;
    const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
    const existingData = result.rowData;

    if (statusPembayaran) existingData.Status_Pembayaran = statusPembayaran;
    if (statusPengiriman) existingData.Status_Pengiriman = statusPengiriman;
    if (nomorResi) existingData.Nomor_Resi = nomorResi;

    const updatedRow = objectToRow(existingData, headers);
    sheet.getRange(rowIndex, 1, 1, updatedRow.length).setValues([updatedRow]);

    // Jika status pesanan berubah menjadi 'Dibatalkan', kembalikan stok produk
    if (statusPengiriman === 'Dibatalkan' && existingData.Status_Pengiriman !== 'Dibatalkan') {
        const detailTransaksi = getAllData(this.DETAIL_SHEET_NAME).filter(item => item.ID_Transaksi === id);
        detailTransaksi.forEach(item => {
            ProdukService.updateStokProduk(item.ID_Produk, item.Kuantitas); // Tambah stok kembali
        });
    }

    return existingData;
  }
};

// --- Layanan Ulasan (UlasanService.gs) ---
const UlasanService = {
  SHEET_NAME: 'Ulasan',
  ID_COLUMN: 'ID_Ulasan',
  ID_PREFIX: 'ULAS-',

  /**
   * Menambahkan ulasan baru.
   * @param {object} data Data ulasan (id_produk, id_member, rating, komentar).
   * @returns {object} Ulasan yang ditambahkan.
   */
  addUlasan: function(data) {
    const sheet = getSheet(this.SHEET_NAME);
    const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];

    // Validasi input
    if (!data.id_produk || !data.id_member || !data.rating || !data.komentar) {
      throw new Error('Data ulasan tidak lengkap.');
    }
    if (data.rating < 1 || data.rating > 5) {
      throw new Error('Rating harus antara 1 dan 5.');
    }

    const newId = generateUniqueId(this.SHEET_NAME, this.ID_COLUMN, this.ID_PREFIX);

    const newUlasan = {
      [this.ID_COLUMN]: newId,
      ID_Produk: data.id_produk,
      ID_Member: data.id_member,
      Rating: data.rating,
      Komentar: data.komentar,
      Tanggal_Ulasan: new Date().toLocaleString(),
      Status: 'Menunggu' // Default, bisa diubah oleh admin
    };

    sheet.appendRow(objectToRow(newUlasan, headers));

    // Perbarui rating rata-rata produk
    this.updateProdukRating(data.id_produk);

    return newUlasan;
  },

  /**
   * Mengambil semua ulasan (untuk admin).
   * @returns {Array<object>} Daftar ulasan.
   */
  getAllUlasan: function() {
    return getAllData(this.SHEET_NAME);
  },

  /**
   * Mengambil ulasan berdasarkan ID Produk.
   * @param {string} produkId ID Produk.
   * @returns {Array<object>} Daftar ulasan untuk produk tertentu.
   */
  getUlasanByProdukId: function(produkId) {
    const allUlasan = getAllData(this.SHEET_NAME);
    return allUlasan.filter(u => u.ID_Produk === produkId && u.Status === 'Disetujui');
  },

  /**
   * Memperbarui rating rata-rata dan jumlah ulasan produk.
   * Dipanggil setelah ulasan baru ditambahkan atau diubah statusnya.
   * @param {string} produkId ID Produk.
   */
  updateProdukRating: function(produkId) {
    const produkSheet = getSheet(ProdukService.SHEET_NAME);
    const ulasanSheet = getSheet(this.SHEET_NAME);

    const produkResult = findRowById(ProdukService.SHEET_NAME, ProdukService.ID_COLUMN, produkId);
    if (!produkResult) return; // Produk tidak ditemukan

    const allUlasan = getAllData(this.SHEET_NAME);
    const relevantUlasan = allUlasan.filter(u => u.ID_Produk === produkId && u.Status === 'Disetujui');

    let totalRating = 0;
    relevantUlasan.forEach(u => {
      totalRating += parseInt(u.Rating) || 0;
    });

    const newJumlahUlasan = relevantUlasan.length;
    const newRating = newJumlahUlasan > 0 ? (totalRating / newJumlahUlasan).toFixed(1) : 0;

    const produkRowIndex = produkResult.rowIndex;
    const produkHeaders = produkSheet.getRange(1, 1, 1, produkSheet.getLastColumn()).getValues()[0];

    const ratingColIndex = produkHeaders.indexOf('Rating');
    const jumlahUlasanColIndex = produkHeaders.indexOf('Jumlah_Ulasan');
    const lastUpdateColIndex = produkHeaders.indexOf('Last_Update');

    if (ratingColIndex !== -1) {
      produkSheet.getRange(produkRowIndex, ratingColIndex + 1).setValue(newRating);
    }
    if (jumlahUlasanColIndex !== -1) {
      produkSheet.getRange(produkRowIndex, jumlahUlasanColIndex + 1).setValue(newJumlahUlasan);
    }
    if (lastUpdateColIndex !== -1) {
      produkSheet.getRange(produkRowIndex, lastUpdateColIndex + 1).setValue(new Date().toLocaleString());
    }
  }
};



Job Builder

  https://www.youtube.com/watch?v=5b15m4N1Heg