Compare commits

..

2 Commits

Author SHA1 Message Date
Vassshhh
bfbb750c4d ok 2025-08-25 03:47:47 +07:00
Vassshhh
4ec28f7089 ok 2025-08-23 16:26:47 +07:00
3 changed files with 312 additions and 178 deletions

View File

@@ -38,7 +38,7 @@ function HomePage({
if (tab === 'products') scrollToProduct();
if (tab === 'academy') scrollToCourse();
}, [productSectionRef, courseSectionRef]);
}, [productSectionRef.current, courseSectionRef.current]);
return (
<>
@@ -194,31 +194,42 @@ function App() {
if (!productModalRequest || !subscriptions) return;
const { productId, authorizedUri, unauthorizedUri } = productModalRequest;
console.log(subscriptions)
const hasAccess = subscriptions && subscriptions.some(
sub => sub.product_id === productId || sub.product_parent_id === productId
);
console.log(hasAccess)
console.log("hasAccess:", hasAccess);
if (hasAccess) {
if (authorizedUri) {
let finalUri = decodeURIComponent(authorizedUri);
const token = document.cookie.match(/(^| )token=([^;]+)/)?.[2];
if (finalUri.includes('token=null') || finalUri.includes('token=')) {
// --- ambil product_name distinct berdasarkan productId/parent ---
const relatedSubs = subscriptions.filter(
sub => sub.product_id === productId || sub.product_parent_id === productId
);
const distinctNames = [...new Set(relatedSubs.map(sub => sub.product_name))];
if (distinctNames.length > 1) {
// lebih dari 1 → pakai dashboard=true
const url = new URL(finalUri);
url.searchParams.set('token', token || '');
url.searchParams.set("token", token || "");
finalUri = url.toString();
} else if (distinctNames.length === 1) {
// hanya 1 → tambahkan productName=<nama> sebelum query lain
const url = new URL(finalUri);
url.searchParams.set("token", token || "");
url.searchParams.set("productName", distinctNames[0]);
finalUri = url.toString();
}
window.location.href = finalUri;
}
else {// Assuming you already imported processProducts from './processProducts'
} else {
// fallback ambil detail produk via fetch
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
itemsId: [productId],
withChildren: true,
@@ -227,9 +238,7 @@ function App() {
.then(res => res.json())
.then(data => {
if (Array.isArray(data) && data.length > 0) {
// Process the raw data to group children under their parent
const processed = processProducts(data);
// Set the first product (which should be the parent with children nested)
setSelectedProduct(processed[0]);
setShowedModal('product');
}
@@ -240,12 +249,9 @@ function App() {
if (unauthorizedUri) {
window.location.href = decodeURIComponent(unauthorizedUri);
} else {
fetch('https://bot.kediritechnopark.com/webhook/store-production/products', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
itemsId: [productId],
withChildren: true,
@@ -254,9 +260,7 @@ function App() {
.then(res => res.json())
.then(data => {
if (Array.isArray(data) && data.length > 0) {
// Process the raw data to group children under their parent
const processed = processProducts(data);
// Set the first product (which should be the parent with children nested)
setSelectedProduct(processed[0]);
setShowedModal('product');
}

View File

@@ -38,13 +38,36 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
}
// Auto check saat user mengetik (debounce)
useEffect(() => {
if (product.unique_name == false) return;
const name = customName.trim();
if (!name) {
setStatus('idle');
return;
}
const isNameTaken = subscriptions?.some(sub => {
const isSameProduct = sub.product_id === product.id || sub.product_parent_id === product.id;
if (!isSameProduct) return false;
const existingName = sub.product_name?.split('%%%')[0]?.trim().toLowerCase();
return existingName === name.toLowerCase();
});
if (subscriptions && isNameTaken) {
setStatus('unavailable');
return;
}
else if(!product.unique_name){
setStatus('available');
return;
}
if (!product.unique_name) {
console.log(subscriptions);
return;
}
let cancelled = false;
setStatus('checking');
@@ -79,7 +102,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
subscriptions.some(sub =>
String(sub.product_id) === String(product.id) || String(sub.product_parent_id) === String(product.id)
);
console.log(hasMatchingSubscription)
// ✅ Check subscription first
if (hasMatchingSubscription) {
const matching = subscriptions.filter(sub =>
@@ -96,7 +119,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
return;
} else {
const itemsParam = JSON.stringify([product.id]);
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${product.name}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${product.name}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
return;
}
}
@@ -108,7 +131,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
// Fallback: direct checkout
const itemsParam = JSON.stringify([product.id]);
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
};
// ✅ Confirm child selection (final step after naming)
@@ -124,7 +147,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
const encodedName = encodeURIComponent(customName.trim() || product.name);
const itemsParam = JSON.stringify(selectedChildIds);
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
};
// ✅ User sets name first → then if product has children, show child selector
@@ -149,7 +172,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
const itemsParam = JSON.stringify([product.id]);
const encodedName = encodeURIComponent(customName.trim());
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&new_name=${encodedName}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
};
const onConfirmSelector = () => {
@@ -171,14 +194,26 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
const productName = selectedSubscription?.product_name;
const encodedName = encodeURIComponent(productName);
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${encodedName}&redirect_uri=https://kediritechnopark.com/products&redirect_failed=https://kediritechnopark.com`;
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${encodedName}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
}
};
useEffect(() => {
if (willDo === 'checkout') {
if (!product.executeCheckout && willDo === 'checkout') {
onCheckout();
}
else if (product.children && product.children.length > 0) {
setShowChildSelector(true);
}
else {
const tokenCookie = document.cookie.split('; ').find(row => row.startsWith('token='));
const token = tokenCookie ? tokenCookie.split('=')[1] : '';
const encodedName = encodeURIComponent(product.name);
const itemsParam = JSON.stringify([product.id]);
window.location.href = `https://checkout.kediritechnopark.com/?token=${token}&itemsId=${itemsParam}&set_name=${encodedName}&redirect_uri=https://kediritechnopark.com/dashboard&redirect_failed=https://kediritechnopark.com`;
}
if (setWillDo) setWillDo('');
}, []);
@@ -318,7 +353,7 @@ const ProductDetail = ({ willDo, setWillDo, subscriptions, product, requestLogin
style={{ width: '100%', padding: '8px', marginBottom: '8px', borderRadius: '10px' }}
/>
{product.unique_name && <StatusLine />}
<StatusLine />
<div className={styles.buttonGroup} style={{ marginTop: 12 }}>
<button className={styles.button} onClick={() => setShowNamingInput(false)}>

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect } from "react";
import { Container, Row, Col, Card, Button, Tabs, Tab, Form } from "react-bootstrap";
import { Container, Row, Col, Card, Button, Tabs, Tab, Form, ListGroup, Badge, Accordion } from "react-bootstrap";
import { useNavigate } from "react-router-dom";
import processProducts from '../../helper/processProducts';
const Dashboard = ({
subscriptions,
@@ -28,7 +27,31 @@ const Dashboard = ({
}
});
// 🔹 Handle input settings
const [purchaseHistory, setPurchaseHistory] = useState([]);
useEffect(() => {
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
const token = match ? match[2] : null;
if (!token) {
console.error("Token not found in cookies");
return;
}
fetch("https://bot.kediritechnopark.com/webhook/store-production/history", {
headers: {
Authorization: `Bearer ${token}`
}
})
.then((res) => {
if (!res.ok) throw new Error("Failed to fetch purchase history");
return res.json();
})
.then((data) => setPurchaseHistory(data))
.catch((err) => console.error("Error fetching purchase history:", err));
}, []);
const handleSettingsChange = (field, value, nested = false) => {
if (nested) {
setSettings((prev) => ({
@@ -40,7 +63,6 @@ const Dashboard = ({
}
};
// 🔹 Save profile
const saveSettings = () => {
fetch("https://bot.kediritechnopark.com/webhook-test/user-production/data", {
method: "PUT",
@@ -52,7 +74,6 @@ const Dashboard = ({
.catch((err) => alert("Error updating settings: " + err));
};
// 🔹 Group subscriptions
const groupSubscriptionsByProductName = (subs) => {
const result = {};
subs.forEach((sub) => {
@@ -82,7 +103,6 @@ const Dashboard = ({
return result;
};
// 🔹 Fetch produk berdasarkan subscription
useEffect(() => {
if (!subscriptions) return;
@@ -96,40 +116,39 @@ const Dashboard = ({
})
.then((res) => res.json())
.then((data) => {
console.log(data)
const dataMap = {};
data.forEach((item) => {
dataMap[item.id] = { ...item, children: [] };
});
// Masukkan anak-anak ke parent-nya
data.forEach((item) => {
if (item.sub_product_of && dataMap[item.sub_product_of]) {
dataMap[item.sub_product_of].children.push(dataMap[item.id]);
}
});
console.log(dataMap)
const enrichedData = Object.values(groupedSubs)
.filter((group) => data.some((p) => p.id === group.product_id))
.map((group) => {
const productData = data.find((p) => p.id === group.product_id);
let image = productData?.image || "";
let description = productData?.description || "";
let description = "";
console.log(productData)
let realProductName = productData?.name || "";
let site_url = productData?.site_url || "";
if (!image && productData?.sub_product_of) {
if (productData?.sub_product_of) {
const parent = data.find((p) => p.id === productData.sub_product_of);
image = parent?.image || "";
realProductName = parent.name;
description = parent?.description || "";
site_url = parent?.site_url || "";
}
return {
executeCheckout: group.product_name,
id: group.product_id,
name: group.product_name,
realProductName,
type: productData?.type || "product",
image,
description,
site_url,
price: productData?.price || 0,
@@ -143,26 +162,21 @@ const Dashboard = ({
children: dataMap[productData?.sub_product_of]?.children || []
};
});
console.log(enrichedData)
setProducts(enrichedData);
})
.catch((err) => console.error("Fetch error:", err));
}, [subscriptions]);
return (
<Container fluid className="mt-4">
<Tabs
activeKey={activeTab}
onSelect={(k) => setActiveTab(k)}
className="mb-3"
>
{/* Produk Saya */}
<Container fluid className="py-4 px-3 px-md-5">
<Tabs activeKey={activeTab} onSelect={(k) => setActiveTab(k)} className="mb-3">
<Tab eventKey="products" title="Produk Saya">
<Row>
{products.map((product) => (
<Col md={4} key={product.id} className="mb-4">
{products.map((product, i) => (
<Col md={4} key={i} className="mb-4">
<Card
className={`h-100 shadow-sm ${hoveredCard === product.name ? "border-primary" : ""}`}
className={`h-100 shadow-sm p-2 ${hoveredCard === product.name ? "border-primary" : ""}`}
onMouseEnter={() => setHoveredCard(product.name)}
onMouseLeave={() => setHoveredCard(null)}
onClick={() => {
@@ -170,26 +184,23 @@ const Dashboard = ({
setShowedModal("product");
}}
>
{product.image && (
<Card.Img variant="top" src={product.image} />
)}
<Card.Body>
<Card.Title>{product.name.split("%%%")[0]}</Card.Title>
<Card.Text>{product.description}</Card.Text>
<Card.Title>
📦 &nbsp; {product.name.split("%%%")[0] + ' | ' + product.realProductName}
</Card.Title>
<Card.Text style={{ fontSize: "0.9rem", color: "#555" }}>
{product.description}
</Card.Text>
</Card.Body>
<Card.Footer>
<Card.Footer className="d-flex justify-content-between align-items-center">
<small className="text-muted">
{product.unit_type === "duration"
? `Valid until: ${product.end_date
? new Date(product.end_date).toLocaleDateString()
: "N/A"
}`
: `SISA TOKEN ${product.quantity || 0}`}
? `Valid until: ${product.end_date ? new Date(product.end_date).toLocaleDateString() : "N/A"}`
: `SISA TOKEN: ${product.quantity || 0}`}
</small>
<Button
variant="primary"
variant="outline-primary"
size="sm"
className="float-end"
onClick={() => {
setSelectedProduct(product);
setShowedModal("product");
@@ -203,141 +214,225 @@ const Dashboard = ({
</Col>
))}
{/* Tambah produk baru */}
<Col md={4} className="mb-4">
<Card
className={`h-100 shadow-sm text-center align-items-center justify-content-center`}
className="h-100 shadow-sm d-flex justify-content-center align-items-center text-center"
onClick={() => navigate("/?tab=products")}
>
<Card.Body>
<h5>+ Tambah produk baru</h5>
<h5 style={{ color: "#007bff" }}> Tambah Produk Baru</h5>
</Card.Body>
</Card>
</Col>
</Row>
</Tab>
{/* Profil Pengguna */}
<Tab eventKey="settings" title="Profil Pengguna">
<Form>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Username</Form.Label>
<Form.Control
value={settings.username}
onChange={(e) =>
handleSettingsChange("username", e.target.value)
}
/>
</Form.Group>
</Col>
<Tab eventKey="settings" title="Profil">
<Card className="shadow-sm border-0">
<Card.Body>
<Card.Title className="mb-4">Pengaturan Profil</Card.Title>
<Form>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Username</Form.Label>
<Form.Control
value={settings.username}
onChange={(e) =>
handleSettingsChange("username", e.target.value)
}
/>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Email</Form.Label>
<Form.Control
value={settings.email}
onChange={(e) =>
handleSettingsChange("email", e.target.value)
}
/>
</Form.Group>
</Col>
</Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Email</Form.Label>
<Form.Control
value={settings.email}
onChange={(e) =>
handleSettingsChange("email", e.target.value)
}
/>
</Form.Group>
</Col>
</Row>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Nama Lengkap</Form.Label>
<Form.Control
value={settings.profile_data.name}
onChange={(e) =>
handleSettingsChange("name", e.target.value, true)
}
/>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>No. HP</Form.Label>
<Form.Control
value={settings.profile_data.phone}
onChange={(e) =>
handleSettingsChange("phone", e.target.value, true)
}
/>
</Form.Group>
</Col>
</Row>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Full Name</Form.Label>
<Form.Label>Alamat</Form.Label>
<Form.Control
value={settings.profile_data.name}
as="textarea"
rows={2}
value={settings.profile_data.address}
onChange={(e) =>
handleSettingsChange("name", e.target.value, true)
handleSettingsChange("address", e.target.value, true)
}
/>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Phone</Form.Label>
<Form.Control
value={settings.profile_data.phone}
onChange={(e) =>
handleSettingsChange("phone", e.target.value, true)
}
/>
</Form.Group>
</Col>
</Row>
<Form.Group className="mb-3">
<Form.Label>Address</Form.Label>
<Form.Control
value={settings.profile_data.address}
onChange={(e) =>
handleSettingsChange("address", e.target.value, true)
}
/>
</Form.Group>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Perusahaan</Form.Label>
<Form.Control
value={settings.profile_data.company}
onChange={(e) =>
handleSettingsChange("company", e.target.value, true)
}
/>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>URL Gambar Profil</Form.Label>
<Form.Control
value={settings.profile_data.image}
onChange={(e) =>
handleSettingsChange("image", e.target.value, true)
}
/>
</Form.Group>
</Col>
</Row>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Company</Form.Label>
<Form.Control
value={settings.profile_data.company}
onChange={(e) =>
handleSettingsChange("company", e.target.value, true)
}
/>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Profile Image URL</Form.Label>
<Form.Control
value={settings.profile_data.image}
onChange={(e) =>
handleSettingsChange("image", e.target.value, true)
}
/>
</Form.Group>
</Col>
</Row>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Password Baru</Form.Label>
<Form.Control
type="password"
value={settings.password}
onChange={(e) =>
handleSettingsChange("password", e.target.value)
}
/>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Ketik Ulang Password</Form.Label>
<Form.Control type="password" />
</Form.Group>
</Col>
</Row>
<Row>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>New Password</Form.Label>
<Form.Control
type="password"
value={settings.password}
onChange={(e) =>
handleSettingsChange("password", e.target.value)
}
/>
</Form.Group>
</Col>
<Col md={6}>
<Form.Group className="mb-3">
<Form.Label>Re-type New Password</Form.Label>
<Form.Control type="password" />
</Form.Group>
</Col>
</Row>
<Button variant="success" onClick={saveSettings}>
Save Changes
</Button>
</Form>
<Button variant="success" onClick={saveSettings}>
Simpan Perubahan
</Button>
</Form>
</Card.Body>
</Card>
</Tab>
{/* Orders */}
<Tab eventKey="orders" title="Pembelian">
<h4>My Orders</h4>
<p>Orders list will be displayed here.</p>
<Card className="shadow-sm border-0">
<Card.Body>
<Card.Title className="mb-4">Riwayat Pembelian</Card.Title>
{purchaseHistory.length === 0 ? (
<p className="text-muted">Tidak ada riwayat pembelian.</p>
) : (
<Accordion defaultActiveKey={null} alwaysOpen>
{purchaseHistory
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
.map((order, idx) => {
const createdAt = new Date(order.created_at);
// Konversi status ke label yang lebih ramah pengguna
const statusLabel = {
failed: "Transaksi Dibatalkan",
pending: "Menunggu Pembayaran",
completed: "Sukses",
}[order.status] || order.status;
// Tentukan warna/status visual
const statusVariant = {
failed: "danger",
pending: "warning",
completed: "success",
}[order.status] || "secondary";
return (
<Accordion.Item eventKey={String(idx)} key={order.transaction_id + "-" + order.detailed_transactions_id}>
<Accordion.Header>
<div className="d-flex flex-column flex-md-row justify-content-between w-100">
<div>
<strong>{order.product_name.replace(/\t/g, " ")}</strong>
<div className="text-muted small">
ID: {order.transaction_id} {createdAt.toLocaleString("id-ID")}
</div>
</div>
<div className="text-md-end mt-2 mt-md-0">
<Badge bg={statusVariant} className="text-capitalize">
{statusLabel}
</Badge>
<div>
<strong className="ms-2">Rp {order.amount.toLocaleString("id-ID")}</strong>
</div>
</div>
</div>
</Accordion.Header>
<Accordion.Body>
<Row>
<Col md={6}>
<p className="mb-1"><strong>Metode Pembayaran:</strong> {order.payment_method || "N/A"}</p>
<p className="mb-1"><strong>Status:</strong> {statusLabel}</p>
<p className="mb-1"><strong>Tanggal Transaksi:</strong> {createdAt.toLocaleString("id-ID")}</p>
</Col>
<Col md={6} className="text-md-end">
{order.status == 'failed' && (
<div>
<p className="text-danger small">
Jika Anda sudah melakukan pembayaran, silakan konfirmasi manual.
</p>
<Button
variant="outline-danger"
size="sm"
onClick={() => {
alert("Fungsi konfirmasi manual belum diimplementasikan.");
}}
>
Konfirmasi Manual
</Button>
</div>
)}
</Col>
</Row>
</Accordion.Body>
</Accordion.Item>
);
})}
</Accordion>
)}
</Card.Body>
</Card>
</Tab>
</Tabs>
</Container>
);