// 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());
}
}
};