Platform Produk Digital
Harga Terjangkau

0
Pelanggan
0
Aktivasi
0
Rating

Kategori Produk

Produk Populer

Semua Produk

Produk tidak ditemukan

Coba kata kunci lain atau lihat semua produk.

Cek Status Akun Kamu

Ketik email, username, atau nama yang dipakai saat beli

Lagi nyari akunmu… main dulu yuk!

Panduan Instalasi

Langkah lengkap aktivasi & install, dari awal sampai selesai

FAQ

Hal-hal yang sering ditanyain sebelum beli

Download Software Resmi

Link langsung dari server Microsoft, bukan pihak ketiga

Semua link di atas adalah link resmi dari server Microsoft

Apa Kata Mereka

Ribuan pelanggan udah buktiin sendiri

0
Produk Tersedia
0
Pelanggan Puas
0
Rating Rata-rata
0
Rata-rata Aktivasi

Kenapa Serabut Store?

Bukan sekadar toko, ini alasan kenapa 10K+ orang percaya kami

Metode Pembayaran

Bayar pakai metode apapun yang paling nyaman buat kamu

Virtual Account E-Wallet QRIS Minimarket
Serabut
Serabut Store
serabut.id
100% Original Transaksi Aman Aktivasi Instan
Hubungi Kami
Jakarta Selatan, DKI Jakarta

Panduan

Tutorial instalasi & penggunaan software

Cek Status Akun

Cari menggunakan nama, email, atau nomor WhatsApp

Akun tidak ditemukan
Coba dengan kata kunci lain,
atau hubungi Live Agent kami
Hubungi Live Agent
Akun Saya
★ Admin

pesanan selesai
Nama
Email
WhatsApp
Tgl Lahir
Kelamin
Provinsi

Edit Profil

+62

Pertanyaan yang Sering Ditanya

Temukan jawaban cepat seputar produk & layanan Serabut Store

Masih ada pertanyaan?

CS kami online setiap hari 09.00 – 22.00 WIB

Detail Pesanan

Konfirmasi Pesanan
Admin Dashboard
Sera
Sera AI Assistant
Online · Siap membantu
Sera
SERA

Tanya Sera
Keranjang
Keranjang masih kosong
Total ( item)
`; }, async downloadInvoice(order) { const logo = await this._getLogoDataURL(); const items = order.items || [{ produk: order.produk, varian: order.varian, masaAktif: order.masaAktif, harga: order.harga }]; const html = this._buildInvoiceDoc({ orderId: order.orderId, tanggal: order.tanggal, nama: this.currentUser?.nama || order.nama || '–', email: this.currentUser?.email || '', noWA: this.currentUser?.noWA || '', items, total: order.total || items.reduce((s,i)=>s+Number(i.harga||0),0), paymentMethod: order.paymentMethod, docType: 'invoice', logoSrc: logo, }); const win = window.open('', '_blank', 'width=760,height=900,scrollbars=yes'); win.document.write(html); win.document.close(); }, async adminDownloadInvoice(group) { const logo = await this._getLogoDataURL(); const html = this._buildInvoiceDoc({ orderId: group.orderId, tanggal: group.tanggal, nama: group.nama || '–', email: group.email || '', noWA: group.noWA || '', items: group.items || [], total: group.total, paymentMethod: (group.items||[])[0]?.paymentMethod, docType: 'invoice', logoSrc: logo, }); const win = window.open('', '_blank', 'width=760,height=900,scrollbars=yes'); win.document.write(html); win.document.close(); }, adminDownloadQuotation(group) { const html = this._buildInvoiceDoc({ orderId: group.orderId, tanggal: group.tanggal, nama: group.nama || '–', email: group.email || '', noWA: group.noWA || '', items: group.items || [], total: group.total, paymentMethod: (group.items||[])[0]?.paymentMethod, docType: 'quotation', }); const win = window.open('', '_blank', 'width=760,height=900,scrollbars=yes'); win.document.write(html); win.document.close(); }, // ── Quotation builder methods ──────────────────────────────────── quoReset() { this.quoForm = { nama:'', email:'', noHP:'', alamat:'', payMethod:'Transfer Bank', bankName:'SeaBank', bankAccount:'901000004087', bankHolder:'Eko Harianto', note:'', discountType:'nominal', discountVal:0, items:[] }; this._quoId = null; this.quoProductSearch = -1; this.quoSearchQuery = ''; this.quoFilteredProducts = []; this.quoSendMsg = ''; this.quoView = 'form'; }, quoNewForm() { this.quoReset(); this.quoView = 'form'; }, quoBackToList() { this.quoView = 'list'; }, async quoSaveDraft() { this._quoEnsureId(); this._quoSaveToList('draft'); this.quoSavingSending = true; this.quoSendMsg = ''; try { await this.gasPost({ action: 'saveQuotation', adminEmail: this.currentUser?.email, adminToken: this.currentUser?.sessionToken, quoId: this._quoId, nama: this.quoForm.nama, email: this.quoForm.email, noHP: this.quoForm.noHP, total: this.quoTotal, itemCount: this.quoForm.items.length, status: 'draft', formDataJson: JSON.stringify(this.quoForm), }); this.quoSendMsg = '✓ Tersimpan — ' + this._quoId; } catch { this.quoSendMsg = '✗ Gagal menyimpan'; } finally { this.quoSavingSending = false; setTimeout(() => { this.quoSendMsg = ''; }, 3000); } }, _quoSaveToList(status) { const f = this.quoForm; const existing = this.quoList.findIndex(q => q.id === this._quoId); const entry = { id: this._quoId, tanggal: new Date().toISOString().slice(0,10), nama: f.nama, email: f.email, noHP: f.noHP, total: this.quoTotal, itemCount: f.items.length, status, formData: JSON.parse(JSON.stringify(f)), }; if (existing >= 0) { this.quoList[existing] = {...this.quoList[existing], ...entry}; } else { this.quoList.unshift(entry); } this.quoList = this.quoList.slice(0, 200); }, async quoOpenDraft(q) { if (q.formData) { this.quoForm = JSON.parse(JSON.stringify(q.formData)); this._quoId = q.id; this.quoSendMsg = ''; this.quoView = 'form'; this.quoRecalc(); return; } // Fetch formDataJson dari GAS this.quoListLoading = true; try { const res = await this.gasPost({ action:'getPublicQuo', quoId: q.id }); if (res.success && res.formDataJson) { this.quoForm = JSON.parse(res.formDataJson); q.formData = JSON.parse(JSON.stringify(this.quoForm)); // cache lokal } else { this.quoForm.nama = q.nama || ''; this.quoForm.email = q.email || ''; } } catch { this.quoForm.nama = q.nama || ''; } this.quoListLoading = false; this._quoId = q.id; this.quoSendMsg = ''; this.quoView = 'form'; this.quoRecalc(); }, async _getLogoDataURL() { if (this._logoDataURL) return this._logoDataURL; try { const resp = await fetch('/logo.png'); if (!resp.ok) return null; const blob = await resp.blob(); return new Promise(res => { const r = new FileReader(); r.onload = e => { this._logoDataURL = e.target.result; res(e.target.result); }; r.readAsDataURL(blob); }); } catch { return null; } }, quoAddItem() { this.quoForm.items.push({ deskripsi:'', qty:1, harga:0 }); }, quoRecalc() { /* computed via getters, nothing needed */ }, quoFilterProducts() { const q = (this.quoSearchQuery||'').toLowerCase().trim(); if(!q) { this.quoFilteredProducts = []; return; } this.quoFilteredProducts = (this.products||[]).filter(p => p.nama.toLowerCase().includes(q) || (p.varian||'').toLowerCase().includes(q) || (p.masaAktif||'').toLowerCase().includes(q) ); }, quoSelectProduct(idx, p) { const normName = p.nama.toLowerCase(); const normVarian = (p.varian||'').toLowerCase(); const skipVarian = !normVarian || normVarian==='-' || normName.includes(normVarian); const parts = [p.nama, !skipVarian?p.varian:'', p.masaAktif&&p.masaAktif!=='-'?p.masaAktif:''].filter(Boolean); const desc = parts.join(' · '); this.quoForm.items[idx].deskripsi = desc; this.quoForm.items[idx].harga = Number(p.harga||0); this.quoProductSearch = -1; this.quoFilteredProducts = []; this.quoSearchQuery = ''; }, _quoEnsureId() { if (!this._quoId) { const now = new Date(), pad = n => String(n).padStart(2,'0'); this._quoId = 'QUO-' + now.getFullYear() + pad(now.getMonth()+1) + pad(now.getDate()) + '-' + Math.floor(Math.random()*9000+1000); } }, async _quoBuildDoc(logoDataURL) { const f = this.quoForm; const now = new Date(), pad = n => String(n).padStart(2,'0'); this._quoEnsureId(); const todayStr = `${now.getFullYear()}-${pad(now.getMonth()+1)}-${pad(now.getDate())}`; const items = f.items.map(i => ({ produk: i.deskripsi, varian:'-', masaAktif:'-', harga: Number(i.harga||0) * Number(i.qty||1), qty: i.qty, })); const logo = logoDataURL || await this._getLogoDataURL(); return this._buildInvoiceDoc({ orderId: this._quoId, tanggal: todayStr, nama: f.nama, email: f.email, noWA: '62'+String(f.noHP||'').replace(/\D/g,''), alamat: f.alamat, items, total: this.quoTotal, subtotal: this.quoSubtotal, discountAmt: this.quoDiscountAmount, paymentMethod: f.payMethod, bankName: f.bankName, bankAccount: f.bankAccount, bankHolder: f.bankHolder, note: f.note, docType: 'quotation', logoSrc: logo, }); }, async quoGenerate() { this._quoEnsureId(); const logo = await this._getLogoDataURL(); const html = await this._quoBuildDoc(logo); const win = window.open('', '_blank', 'width=800,height=960,scrollbars=yes'); win.document.write(html); win.document.close(); this._quoSaveToList('preview'); // simpan ke GAS Sheet agar link preview bisa diakses this.gasPost({ action: 'saveQuotation', adminEmail: this.currentUser?.email, adminToken: this.currentUser?.sessionToken, quoId: this._quoId, nama: this.quoForm.nama, email: this.quoForm.email, noHP: this.quoForm.noHP, total: this.quoTotal, itemCount: this.quoForm.items.length, status: 'preview', formDataJson: JSON.stringify(this.quoForm), }).catch(() => {}); }, async quoSendWA() { const f = this.quoForm; const rawNum = String(f.noHP||'').replace(/\D/g,''); if (!rawNum || rawNum.length < 8) { this.quoSendMsg = '⚠️ No. HP penerima belum diisi'; return; } this._quoEnsureId(); this.quoWASending = true; this.quoSendMsg = ''; try { // simpan ke Sheet dulu agar link preview bisa diakses await this.gasPost({ action: 'saveQuotation', adminEmail: this.currentUser?.email, adminToken: this.currentUser?.sessionToken, quoId: this._quoId, nama: f.nama, email: f.email, noHP: f.noHP, total: this.quoTotal, itemCount: f.items.length, status: 'sent_wa', formDataJson: JSON.stringify(f), }).catch(() => {}); const logo = await this._getLogoDataURL(); const htmlContent = await this._quoBuildDoc(logo); const fp = v => 'Rp ' + Number(v||0).toLocaleString('id-ID'); const itemLines = f.items.map((it,i) => `${i+1}. ${it.deskripsi} x${it.qty||1} — ${fp(Number(it.harga||0)*Number(it.qty||1))}`).join('\n'); const bankInfo = f.bankName ? `${f.bankName}: ${f.bankAccount} (a.n ${f.bankHolder})` : ''; const quoLink = `https://serabut.id/quo.html?id=${this._quoId}`; const data = await this.gasPost({ action: 'sendQuotationWA', adminEmail: this.currentUser?.email, adminToken: this.currentUser?.sessionToken, noHP: '62' + rawNum.replace(/^0/,''), quoId: this._quoId, nama: f.nama, itemLines, total: this.quoTotal, payMethod: f.payMethod, bankInfo, quoLink, htmlContent, }); if (data.success) { this.quoSendMsg = '✓ WA terkirim ke +62' + rawNum; this._quoSaveToList('sent_wa'); } else { this.quoSendMsg = '✗ ' + (data.error||'Gagal kirim WA'); } } catch { this.quoSendMsg = '✗ Gagal terhubung ke server'; } finally { this.quoWASending = false; } }, async quoSendEmail() { const f = this.quoForm; if (!f.email) { this.quoSendMsg = '⚠️ Email penerima belum diisi'; return; } this._quoEnsureId(); this.quoEmailSending = true; this.quoSendMsg = ''; try { // simpan ke GAS Sheet dulu agar link valid saat dibuka await this.gasPost({ action: 'saveQuotation', adminEmail: this.currentUser?.email, adminToken: this.currentUser?.sessionToken, quoId: this._quoId, nama: f.nama, email: f.email, noHP: f.noHP, total: this.quoTotal, itemCount: f.items.length, status: 'sent_email', formDataJson: JSON.stringify(this.quoForm), }).catch(() => {}); const quoLink = `https://serabut.id/quo.html?id=${this._quoId}`; const data = await this.gasPost({ action: 'sendQuotationEmail', adminEmail: this.currentUser?.email, adminToken: this.currentUser?.sessionToken, to: f.email, quoId: this._quoId, nama: f.nama, subject: `Penawaran ${this._quoId}${f.nama?' untuk '+f.nama:''} dari Serabut Store`, quoLink, }); if (data.success) { this.quoSendMsg = '✓ Email terkirim ke ' + f.email; this._quoSaveToList('sent_email'); } else { this.quoSendMsg = '✗ ' + (data.error||'Gagal kirim email'); } } catch { this.quoSendMsg = '✗ Gagal terhubung ke server'; } finally { this.quoEmailSending = false; } }, // ───────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────── async adminUpdateGroupStatus(group, newStatus, payMethod) { if(!newStatus) return; await Promise.all(group.items.map(item => this.adminUpdateOrderStatus(item, newStatus, true, payMethod))); group.status = newStatus; group.items.forEach(i => { i.status = newStatus; if(payMethod) i.paymentMethod = payMethod; }); this._adminShowMsg(`${group.orderId} (${group.items.length} item) → ${newStatus}${payMethod?' via '+payMethod:''} ✓`); }, async adminSavePayMethod(group) { const pm = this.adminPayMethodVal.trim(); if (!pm) { this._adminShowMsg('Pilih metode pembayaran dulu'); return; } this.adminPayMethodSaving = group.orderId; try { await Promise.all(group.items.map(item => this.gasPost({ action:'updateOrderStatus', adminEmail:this.currentUser.email, adminToken:this.currentUser.sessionToken, rowIndex:item.rowIndex, status:item.status, paymentMethod:pm, skipNotify:true }) )); group.items.forEach(i => { i.paymentMethod = pm; }); this.adminPayMethodEdit = null; this._adminShowMsg(`${group.orderId} metode bayar → ${pm} ✓`); const buyerOrder = this.orders.find(o => o.orderId === group.orderId); if (buyerOrder) { buyerOrder.paymentMethod = pm; buyerOrder.items?.forEach(i => { i.paymentMethod = pm; }); } } catch { this._adminShowMsg('Gagal update metode bayar'); } finally { this.adminPayMethodSaving = null; } }, // ── getCategoryIconSVG (icon preset) ────────── getCategoryIconSVG(key) { const icons = { office365: ``, adobe: ``, windows: ``, office: ``, google: ``, coreldraw: ``, project: ``, visio: ``, other: ``, }; return icons[key] || icons['other']; }, // ── Order date helpers ───────────────────────────── parseTanggalWIB(str) { // Handle "yyyy-MM-dd HH:mm" (ISO) dan "dd/MM/yyyy HH:mm" (lama) if (!str) return null; const s = String(str).trim(); const [datePart, timePart] = s.split(' '); const [h, min] = (timePart||'00:00').split(':'); let d, m, y; if (datePart && datePart.indexOf('-') >= 4) { // ISO format: yyyy-MM-dd [y, m, d] = datePart.split('-'); } else { // Legacy: dd/MM/yyyy [d, m, y] = (datePart||'').split('/'); } if (!y) return null; return new Date(Date.UTC(+y, +m - 1, +d, +h - 7, +min || 0)); }, orderMsLeft(order) { const created = this.parseTanggalWIB(order?.tanggal); if (!created) return 0; const raw = created.getTime() + 24 * 3600 * 1000 - this.now; if (raw <= 0) return 0; // Guard: date di GSheet salah (tanggal masa depan → raw > 24h) // Hitung mundur dari saat halaman dibuka agar countdown tetap jalan if (raw > 24 * 3600 * 1000) { return Math.max(0, this._pageLoadTime + 24 * 3600 * 1000 - this.now); } return raw; }, orderTimeLeftStr(order) { const ms = order?.msecLeft ?? this.orderMsLeft(order); if (ms <= 0) return null; const h = Math.floor(ms / 3600000); const m = Math.floor((ms % 3600000) / 60000); return h + 'j ' + m + 'm'; }, // ── Open order detail (auto-sync jika Pending atau Dibatalkan) ── openOrderDetail(order) { this.orderDetailModal = order; this.orderDetailSyncing = false; // Reset inline review form this.reviewRating = 0; this.reviewHover = 0; this.reviewKomentar = ''; this.reviewAnonim = false; this.reviewError = ''; this.reviewSuccess = false; this.reviewOrderId = ''; this.reviewEditMode = false; // Auto-sync: Pending dalam 24 jam, atau Dibatalkan (mungkin korban auto-cancel bug) if (order.status === 'Pending' || order.status === 'Dibatalkan') { this.checkOrderPaymentStatus(order, true); } }, // ── Cek status ke iPaymu & update UI jika sudah dibayar ── async checkOrderPaymentStatus(order, silent = false) { if (!this.currentUser || !order) return; this.orderDetailSyncing = true; try { const data = await this.gasPost({ action: 'checkIPaymuOrderStatus', orderId: order.orderId, email: this.currentUser.email, sessionToken: this.currentUser.sessionToken }); if (data.success && data.paid) { const updated = { ...order, status: data.orderStatus || 'Diproses', paymentMethod: data.paymentMethod || '', paymentStatus: data.paymentStatus || 'Berhasil' }; const idx = this.orders.findIndex(o => o.orderId === order.orderId); if (idx >= 0) this.orders[idx] = updated; if (this.orderDetailModal?.orderId === order.orderId) this.orderDetailModal = updated; } else if (!silent && data.error) { console.warn('[checkOrderPaymentStatus]', data.error); } } catch(e) { console.error('[checkOrderPaymentStatus]', e); } this.orderDetailSyncing = false; }, // ── Lanjutkan pembayaran Pending ── async continuePendingPayment(order) { if (!this.currentUser || !order) return; this.orderDetailSyncing = true; try { // Cek dulu apakah sudah dibayar const statusData = await this.gasPost({ action: 'checkIPaymuOrderStatus', orderId: order.orderId, email: this.currentUser.email, sessionToken: this.currentUser.sessionToken }); if (statusData.success && statusData.paid) { const updated = { ...order, status: statusData.orderStatus || 'Diproses', paymentMethod: statusData.paymentMethod || '', paymentStatus: 'Berhasil' }; const idx = this.orders.findIndex(o => o.orderId === order.orderId); if (idx >= 0) this.orders[idx] = updated; this.orderDetailModal = updated; this.orderDetailSyncing = false; return; } // Buat invoice Xendit baru const data = await this.gasPost({ action: 'createXenditInvoice', orderId: order.orderId, items: order.items.map(i => ({ produk: i.produk, varian: i.varian || '-', masaAktif: i.masaAktif || '-', harga: i.harga, qty: 1 })), buyerName: this.currentUser.nama || '', buyerEmail: this.currentUser.email || '', buyerPhone: this.currentUser.wa || '', total: order.total }); if (data.success && data.paymentUrl) { this.orderDetailModal = null; this.openPaymentIframe(order.orderId, data.paymentUrl); } else { this._showToast(data.error || 'Gagal membuat sesi pembayaran. Coba lagi atau hubungi CS.', 'error'); } } catch(e) { this._showToast('Gagal terhubung ke server', 'error'); } this.orderDetailSyncing = false; }, async loadOrders(force=false) { if(!this.currentUser || this.ordersLoading) return; // Skip jika sudah loaded dan tidak force — kecuali ada pending order detail if(this.ordersLoaded && !force && !this.pendingOrderDetailId) return; this.ordersLoading=true; this.ordersCurrentPage=1; try { const data = await this.gasPost({ action:'getOrders', email:this.currentUser.email, sessionToken:this.currentUser.sessionToken }); if(!data.success) console.warn('[loadOrders] error dari GAS:', data.error); this.orders = data.data || []; this.ordersLoaded = true; // Load review milik buyer dari GAS (survive refresh & lintas device) this._syncBuyerReviews(); // Auto-open order detail jika kembali dari payment redirect if (this.pendingOrderDetailId) { const targetId = this.pendingOrderDetailId; this.pendingOrderDetailId = null; const order = this.orders.find(o => o.orderId === targetId); if (order) this.$nextTick(() => this.openOrderDetail(order)); } } catch(err) { console.error('[loadOrders] exception:', err); this.orders=[]; } finally { this.ordersLoading=false; } }, async loadProfile() { if(!this.currentUser) return; try { const data = await this.gasPost({ action:'getProfile', email:this.currentUser.email, sessionToken:this.currentUser.sessionToken }); if(data.success) this.profileExtra = data.profile || {}; } catch {} }, openEditMode() { this.editForm = { nama: this.profileExtra.nama || this.currentUser?.nama || '', wa: String(this.currentUser?.wa||'').replace(/^62/, ''), tanggalLahir: this.profileExtra.tanggalLahir || '', jenisKelamin: this.profileExtra.jenisKelamin || '', alamat: this.profileExtra.alamat || '', provinsi: this.profileExtra.provinsi || '', }; this.editError = ''; this.editMode = true; }, async saveProfile() { if(!this.editForm.nama.trim()){ this.editError='Nama tidak boleh kosong'; return; } this.editLoading=true; this.editError=''; try { const data = await this.gasPost({ action: 'updateProfile', email: this.currentUser.email, sessionToken: this.currentUser.sessionToken, nama: this.editForm.nama.trim(), wa: this.editForm.wa ? '62' + this.editForm.wa.replace(/^0+/, '') : '', tanggalLahir: this.editForm.tanggalLahir, jenisKelamin: this.editForm.jenisKelamin, alamat: this.editForm.alamat, provinsi: this.editForm.provinsi, }); if(data.success) { this.currentUser = { ...data.user, sessionToken: this.currentUser.sessionToken }; const { sessionToken: _st5, ...userPub5 } = this.currentUser; localStorage.setItem('serabutUser', JSON.stringify(userPub5)); if(_st5) localStorage.setItem('srb_tok', _st5); this.profileExtra = { ...this.profileExtra, ...this.editForm }; this.editMode = false; } else { this.editError = data.error||'Gagal menyimpan'; } } catch { this.editError='Gagal terhubung ke server'; } this.editLoading=false; }, async submitResetPw() { const {oldPassword,newPassword,konfirmasi} = this.resetPwForm; if(!oldPassword||!newPassword||!konfirmasi){ this.resetPwError='Semua field harus diisi'; return; } if(newPassword.length<6){ this.resetPwError='Password baru minimal 6 karakter'; return; } if(newPassword!==konfirmasi){ this.resetPwError='Konfirmasi password tidak cocok'; return; } this.resetPwLoading=true; this.resetPwError=''; try { const email = this.currentUser.email.toLowerCase().trim(); const oldHash = await sha256(email + ':' + oldPassword); const newHash = await sha256(email + ':' + newPassword); const oldHashLegacy = await sha256(oldPassword); const data = await this.gasPost({ action: 'changePassword', email, sessionToken: this.currentUser.sessionToken, oldPassword: oldHash, newPassword: newHash, oldPasswordLegacy: oldHashLegacy, }); if(data.success) { this.resetPwSuccess=true; // [SEC-05] Server invalidate session — auto logout setelah 2 detik if(data.requireLogin) setTimeout(()=>{ this.logout(); this.openAuthModal('login'); }, 2000); } else { this.resetPwError = data.error||'Gagal mengubah password'; } } catch { this.resetPwError='Gagal terhubung ke server'; } this.resetPwLoading=false; }, // ── Forgot Password (via OTP) ────────────────── openForgotPw() { this.forgotPwModal = true; this.forgotPwStep = 1; this.forgotPwEmail = this.loginForm?.email || ''; this.forgotPwOtp = ''; this.forgotPwNewPw = ''; this.forgotPwConfirm = ''; this.forgotPwError = ''; this.forgotPwSuccess = false; this.authModal = false; document.body.style.overflow = 'hidden'; }, async submitForgotPwEmail() { if (!this.forgotPwEmail.trim()) { this.forgotPwError='Email harus diisi'; return; } this.forgotPwLoading=true; this.forgotPwError=''; try { const data = await this.gasPost({ action:'forgotPasswordSendOTP', email: this.forgotPwEmail.trim().toLowerCase() }); if (data.success) { this.forgotPwMaskedEmail = data.maskedEmail || ''; this.forgotPwMaskedWa = data.maskedWa || ''; this.forgotPwHasWa = !!data.hasWa; this.forgotPwStep = 2; } else { this.forgotPwError = data.error||'Gagal mengirim kode OTP'; } } catch { this.forgotPwError='Gagal terhubung ke server'; } this.forgotPwLoading=false; }, submitForgotPwOtp() { if (!this.forgotPwOtp.trim()) { this.forgotPwError='Kode OTP harus diisi'; return; } if (this.forgotPwOtp.trim().length < 4) { this.forgotPwError='Kode OTP tidak valid'; return; } this.forgotPwError=''; this.forgotPwStep = 3; }, async submitForgotPwNewPw() { if (!this.forgotPwNewPw || !this.forgotPwConfirm) { this.forgotPwError='Semua field harus diisi'; return; } if (this.forgotPwNewPw.length < 6) { this.forgotPwError='Password minimal 6 karakter'; return; } if (this.forgotPwNewPw !== this.forgotPwConfirm) { this.forgotPwError='Konfirmasi password tidak cocok'; return; } this.forgotPwLoading=true; this.forgotPwError=''; try { const email = this.forgotPwEmail.trim().toLowerCase(); const newHash = await sha256(email + ':' + this.forgotPwNewPw); const data = await this.gasPost({ action:'forgotPasswordVerify', email, otp: this.forgotPwOtp.trim(), newPassword: newHash }); if (data.success) { this.forgotPwSuccess=true; } else { this.forgotPwError = data.error||'Gagal reset password'; } } catch { this.forgotPwError='Gagal terhubung ke server'; } this.forgotPwLoading=false; }, async pwaInstall() { if (this.pwaPromptEvent) { this.pwaPromptEvent.prompt(); const { outcome } = await this.pwaPromptEvent.userChoice; if (outcome === 'accepted') localStorage.setItem('pwa_dismissed', Date.now()); this.pwaPromptEvent = null; } this.pwaPopup = false; }, pwaDismiss() { localStorage.setItem('pwa_dismissed', Date.now()); this.pwaPopup = false; }, getStatusIcon(s) { return {Aktif:'✅',Expired:'⏰',Suspended:'🚫',Pending:'⏳'}[s]||'❓'; }, getStatusBg(s) { return {Aktif:'bg-emerald-100',Expired:'bg-orange-100',Suspended:'bg-red-100',Pending:'bg-yellow-100'}[s]||'bg-gray-100'; }, getStatusBadge(s) { return {Aktif:'bg-emerald-100 text-emerald-700',Selesai:'bg-gray-100 text-gray-600',Expired:'bg-orange-100 text-orange-700',Suspended:'bg-red-100 text-red-700',Dibatalkan:'bg-red-100 text-red-600',Pending:'bg-yellow-100 text-yellow-700',Diproses:'bg-indigo-100 text-indigo-700'}[s]||'bg-gray-100 text-gray-600'; }, _orderCardBorder(s) { return {Pending:'border-amber-300',Diproses:'border-indigo-300',Aktif:'border-emerald-300',Selesai:'border-gray-300',Dibatalkan:'border-red-200'}[s]||'border-gray-100'; }, _orderCardHeader(s) { return {Pending:'bg-amber-50 border-amber-200',Diproses:'bg-indigo-50 border-indigo-200',Aktif:'bg-emerald-50 border-emerald-200',Selesai:'bg-gray-50 border-gray-200',Dibatalkan:'bg-red-50 border-red-100'}[s]||'bg-gray-50 border-gray-100'; }, _orderCardFooter(s) { return {Pending:'bg-amber-50/50',Diproses:'bg-indigo-50/50',Aktif:'bg-emerald-50/50',Selesai:'bg-gray-50/50',Dibatalkan:'bg-red-50/50'}[s]||'bg-gray-50/50'; }, _isVal(v) { return v && String(v).trim() !== '' && String(v).trim() !== '-'; }, _adminOrderHasDetails(item) { return this._isVal(item.emailAktif) || this._isVal(item.microsoftEmail) || this._isVal(item.msNama) || this._isVal(item.username) || this._isVal(item.emailReminder); }, adminFormatDate(str) { if (!str) return ''; // Handle "dd/MM/yyyy HH:mm" (GAS format) const m = String(str).match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})\s+(\d{2}:\d{2})/); if (m) { const days = ['Min','Sen','Sel','Rab','Kam','Jum','Sab']; const d = new Date(+m[3], +m[2]-1, +m[1]); return `${days[d.getDay()]}, ${+m[1]} ${['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des'][+m[2]-1]} ${m[3]} ${m[4]} WIB`; } // Handle ISO / JS Date string fallback const d = new Date(str); if (!isNaN(d)) { const days = ['Min','Sen','Sel','Rab','Kam','Jum','Sab']; const mons = ['Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des']; const pad = n => String(n).padStart(2,'0'); return `${days[d.getDay()]}, ${d.getDate()} ${mons[d.getMonth()]} ${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())} WIB`; } return str; }, getStatusMessage(s) { return {Aktif:'Akun kamu aktif dan berjalan normal',Expired:'Masa berlaku habis — segera perpanjang!',Suspended:'Akun dinonaktifkan — hubungi support',Pending:'Akun sedang dalam proses aktivasi'}[s]||'Hubungi support untuk info lebih lanjut'; }, _parseDateStr(str) { if (!str) return null; // DD/MM/YYYY const slash = String(str).split('/'); if (slash.length === 3) return new Date(Number(slash[2]), Number(slash[1])-1, Number(slash[0])); // ISO 8601: "2026-04-08T17:00:00.000Z" atau "2026-04-08" const iso = String(str).match(/^(\d{4})-(\d{2})-(\d{2})/); if (iso) return new Date(Number(iso[1]), Number(iso[2])-1, Number(iso[3])); return null; }, computeSubscriptionStatus(masaBerlaku) { const endDate = this._parseDateStr(masaBerlaku); if (!endDate) return 'Active'; const today = new Date(); today.setHours(0,0,0,0); return endDate >= today ? 'Active' : 'Sudah Dihapus'; }, getDaysUntilExpiry(masaBerlaku) { const endDate = this._parseDateStr(masaBerlaku); if (!endDate) return Infinity; const today = new Date(); today.setHours(0,0,0,0); return Math.floor((endDate - today) / 86400000); }, formatMasaBerlaku(str) { if (!str) return '—'; const bulan = ['','Jan','Feb','Mar','Apr','Mei','Jun','Jul','Agu','Sep','Okt','Nov','Des']; // DD/MM/YYYY const slash = String(str).split('/'); if (slash.length === 3) { const d = parseInt(slash[0]), m = parseInt(slash[1]), y = parseInt(slash[2]); return `${d} ${bulan[m]||''} ${y}`; } // ISO 8601 const iso = String(str).match(/^(\d{4})-(\d{2})-(\d{2})/); if (iso) return `${parseInt(iso[3])} ${bulan[parseInt(iso[2])]||''} ${iso[1]}`; return str; }, copyNewPassword() { navigator.clipboard.writeText(this.statusResetNewPassword).then(() => { this.pwCopied = true; setTimeout(() => { this.pwCopied = false; }, 2000); }); }, async doResetOfficePassword() { if (!this.statusSelected?.officeAccount) return; this.statusResetState = 'loading'; const res = await this.gasPost({ action: 'resetOfficePassword', officeEmail: this.statusSelected.officeAccount }); if (res.success) { this.statusResetNewPassword = res.newPassword; this.statusResetState = 'done'; } else { this.statusResetError = res.error || 'Gagal mereset password'; this.statusResetState = 'error'; } }, statusSuggestRebuy(account) { const durasiNum = parseInt(account.durasi) || 0; const daysLeft = this.getDaysUntilExpiry(account.masaBerlaku); const durationMatch = (p) => { if (!durasiNum) return true; const ma = (p.masaAktif || p.nama || '').toLowerCase(); if (durasiNum >= 12) return ma.includes('year') || ma.includes('tahun') || ma.includes('12'); return ma.includes(String(durasiNum)); }; let matchedProduct = null; if (account.productType === 'adobe') { const nameParts = (account.productName || '').toLowerCase().split(/\s+/).filter(Boolean); matchedProduct = this.products.find(p => { const pn = p.nama.toLowerCase(); if (!pn.includes('adobe')) return false; if (nameParts.length && !nameParts.every(w => pn.includes(w))) return false; return durationMatch(p); }) || this.products.find(p => p.nama.toLowerCase().includes('adobe') && durationMatch(p)); } else { const isFamily = account.productType === 'office365family'; const isRenewal = !isFamily && daysLeft >= 0 && daysLeft <= 7; const tipe = (account.tipe || '').toLowerCase(); matchedProduct = this.products.find(p => { const pn = p.nama.toLowerCase(); if (!pn.includes('365') && !pn.includes('office') && !pn.includes('microsoft')) return false; if (isFamily && !pn.includes('family')) return false; if (!isFamily && pn.includes('family')) return false; if (isRenewal && !pn.includes('renewal')) return false; if (!isRenewal && pn.includes('renewal')) return false; if (tipe === 'web' && !pn.includes('web')) return false; return durationMatch(p); }) || this.products.find(p => { const pn = p.nama.toLowerCase(); if (!pn.includes('365') && !pn.includes('office') && !pn.includes('microsoft')) return false; if (isFamily && !pn.includes('family')) return false; if (!isFamily && pn.includes('family')) return false; if (isRenewal && !pn.includes('renewal')) return false; if (!isRenewal && pn.includes('renewal')) return false; return true; }); } if (matchedProduct) { this.goToProductDetail(matchedProduct); return; } const keyword = account.productType === 'adobe' ? (account.productName || 'Adobe Creative Cloud') : account.productType === 'office365family' ? 'Office 365 Family' : (daysLeft >= 0 && daysLeft <= 7 ? 'Renewal' : 'Office 365'); this.searchQuery = keyword; this.scrollTo('catalog'); }, }; }