ok
This commit is contained in:
@@ -1,8 +1,20 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { TrendingUp, TrendingDown, DollarSign, ShoppingCart, Users } from 'lucide-react';
|
||||
import styles from './Dashboard.module.css';
|
||||
import processProducts from '../helper/processProducts';
|
||||
|
||||
|
||||
const Dashboard = () => {
|
||||
const [unitType, setUnitType] = useState('duration');
|
||||
const [durationUnit, setDurationUnit] = useState('day');
|
||||
const [availableTypes, setAvailableTypes] = useState([]);
|
||||
const [availableGroups, setAvailableGroups] = useState([]);
|
||||
const [selectedType, setSelectedType] = useState(null);
|
||||
const [selectedGroup, setSelectedGroup] = useState(null);
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const [products, setProducts] = useState([]);
|
||||
|
||||
|
||||
const [dashboardData, setDashboardData] = useState({
|
||||
totalRevenue: {
|
||||
amount: 10215845,
|
||||
@@ -24,10 +36,9 @@ const Dashboard = () => {
|
||||
{ date: '22/06', items: 200, revenue: 800 },
|
||||
{ date: '23/06', items: 750, revenue: 450 },
|
||||
{ date: '24/06', items: 550, revenue: 200 },
|
||||
{ date: '24/06', items: 300, revenue: 350 },
|
||||
{ date: '24/06', items: 900, revenue: 450 },
|
||||
{ date: '24/06', items: 550, revenue: 200 },
|
||||
{ date: '24/06', items: 700, revenue: 300 }
|
||||
{ date: '25/06', items: 300, revenue: 350 },
|
||||
{ date: '26/06', items: 900, revenue: 450 },
|
||||
{ date: '27/06', items: 550, revenue: 200 },
|
||||
],
|
||||
latestTransactions: [
|
||||
{
|
||||
@@ -73,58 +84,88 @@ const Dashboard = () => {
|
||||
]
|
||||
});
|
||||
|
||||
// Function untuk connect ke n8n webhook
|
||||
const connectToN8NWebhook = async (webhookUrl) => {
|
||||
try {
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setDashboardData(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error connecting to n8n webhook:', error);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
const fetchDistinctOptions = async () => {
|
||||
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
||||
if (!match) return;
|
||||
const token = match[2];
|
||||
|
||||
try {
|
||||
const res = await fetch('https://bot.kediritechnopark.com/webhook/store-dev/get-products', {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await res.json(); // hasil berupa array produk
|
||||
const products = result || [];
|
||||
|
||||
// Ambil distinct `type` dan `group` manual
|
||||
const types = [...new Set(products.map(p => p.type).filter(Boolean))];
|
||||
const groups = [...new Set(products.map(p => p.group).filter(Boolean))];
|
||||
|
||||
setAvailableTypes(types);
|
||||
setAvailableGroups(groups);
|
||||
setProducts(processProducts(products));
|
||||
} catch (err) {
|
||||
console.error('Gagal ambil produk:', err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchDistinctOptions();
|
||||
}, []);
|
||||
|
||||
|
||||
// Function untuk send data ke n8n webhook
|
||||
const sendDataToN8N = async (webhookUrl, data) => {
|
||||
try {
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('Data sent successfully to n8n');
|
||||
const match = document.cookie.match(new RegExp('(^| )token=([^;]+)'));
|
||||
if (match) {
|
||||
const token = match[2];
|
||||
|
||||
const payload = {
|
||||
...data,
|
||||
duration: data.unit_type === 'token' ? null : data.duration,
|
||||
quantity: data.unit_type === 'duration' ? null : data.quantity,
|
||||
};
|
||||
|
||||
if (!token) {
|
||||
alert('Token tidak ditemukan. Silakan login kembali.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(webhookUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
alert('Dorm berhasil ditambahkan!');
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
console.error('Response Error:', errorText);
|
||||
alert('Gagal mengirim data: ' + response.status);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending data to n8n:', error);
|
||||
alert('Terjadi kesalahan saat mengirim data.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error sending data to n8n:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('id-ID').format(amount);
|
||||
};
|
||||
const formatCurrency = (amount) => new Intl.NumberFormat('id-ID').format(amount);
|
||||
|
||||
const getStatusClass = (status) => {
|
||||
switch (status) {
|
||||
case 'confirmed':
|
||||
return styles.statusConfirmed;
|
||||
case 'waiting payment':
|
||||
return styles.statusWaiting;
|
||||
case 'payment expired':
|
||||
return styles.statusExpired;
|
||||
default:
|
||||
return styles.statusConfirmed;
|
||||
case 'confirmed': return styles.statusConfirmed;
|
||||
case 'waiting payment': return styles.statusWaiting;
|
||||
case 'payment expired': return styles.statusExpired;
|
||||
default: return styles.statusConfirmed;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -134,11 +175,9 @@ const Dashboard = () => {
|
||||
<h3 className={styles.statCardTitle}>{title}</h3>
|
||||
<Icon className={styles.statCardIcon} />
|
||||
</div>
|
||||
|
||||
<div className={styles.statCardValue}>
|
||||
{currency && `${currency} `}{formatCurrency(value)}
|
||||
</div>
|
||||
|
||||
<div className={styles.statCardFooter}>
|
||||
<div className={styles.statCardChange}>
|
||||
{isNegative ? (
|
||||
@@ -152,31 +191,19 @@ const Dashboard = () => {
|
||||
<span className={styles.fromLastWeek}>from last week</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.statCardPeriod}>{period}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const BarChart = ({ data }) => {
|
||||
const maxValue = Math.max(...data.map(item => Math.max(item.items, item.revenue)));
|
||||
|
||||
return (
|
||||
<div className={styles.barChart}>
|
||||
{data.map((item, index) => (
|
||||
<div key={index} className={styles.barGroup}>
|
||||
<div className={styles.barContainer}>
|
||||
<div
|
||||
className={`${styles.bar} ${styles.barItems}`}
|
||||
style={{
|
||||
height: `${(item.items / maxValue) * 200}px`
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.bar} ${styles.barRevenue}`}
|
||||
style={{
|
||||
height: `${(item.revenue / maxValue) * 200}px`
|
||||
}}
|
||||
/>
|
||||
<div className={`${styles.bar} ${styles.barItems}`} style={{ height: `${(item.items / maxValue) * 200}px` }} />
|
||||
<div className={`${styles.bar} ${styles.barRevenue}`} style={{ height: `${(item.revenue / maxValue) * 200}px` }} />
|
||||
</div>
|
||||
<span className={styles.barLabel}>{item.date}</span>
|
||||
</div>
|
||||
@@ -187,78 +214,32 @@ const Dashboard = () => {
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{/* Stats Cards */}
|
||||
<div className={styles.statsGrid}>
|
||||
<StatCard
|
||||
title="Total Revenue"
|
||||
value={dashboardData.totalRevenue.amount}
|
||||
currency={dashboardData.totalRevenue.currency}
|
||||
change={dashboardData.totalRevenue.change}
|
||||
period={dashboardData.totalRevenue.period}
|
||||
icon={DollarSign}
|
||||
isNegative={false}
|
||||
/>
|
||||
<StatCard
|
||||
title="Total Items Sold"
|
||||
value={dashboardData.totalItemsSold.amount}
|
||||
change={dashboardData.totalItemsSold.change}
|
||||
period={dashboardData.totalItemsSold.period}
|
||||
icon={ShoppingCart}
|
||||
isNegative={true}
|
||||
/>
|
||||
<StatCard
|
||||
title="Total Visitor"
|
||||
value={dashboardData.totalVisitors.amount}
|
||||
change={dashboardData.totalVisitors.change}
|
||||
period={dashboardData.totalVisitors.period}
|
||||
icon={Users}
|
||||
isNegative={false}
|
||||
/>
|
||||
<StatCard title="Total Revenue" value={dashboardData.totalRevenue.amount} currency="IDR" change={dashboardData.totalRevenue.change} period={dashboardData.totalRevenue.period} icon={DollarSign} isNegative={false} />
|
||||
<StatCard title="Total Items Sold" value={dashboardData.totalItemsSold.amount} change={dashboardData.totalItemsSold.change} period={dashboardData.totalItemsSold.period} icon={ShoppingCart} isNegative={true} />
|
||||
<StatCard title="Total Visitor" value={dashboardData.totalVisitors.amount} change={dashboardData.totalVisitors.change} period={dashboardData.totalVisitors.period} icon={Users} isNegative={false} />
|
||||
</div>
|
||||
|
||||
{/* Charts and Transactions */}
|
||||
<div className={styles.chartsGrid}>
|
||||
{/* Report Statistics */}
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.chartHeader}>
|
||||
<div>
|
||||
<h3 className={styles.chartTitle}>Report Statistics</h3>
|
||||
<p className={styles.chartSubtitle}>Period: 22 - 29 May 2025</p>
|
||||
</div>
|
||||
<div className={styles.chartLegend}>
|
||||
<div className={styles.legendItem}>
|
||||
<div className={`${styles.legendColor} ${styles.legendColorGreen}`}></div>
|
||||
<span className={styles.legendText}>Items Sold</span>
|
||||
</div>
|
||||
<div className={styles.legendItem}>
|
||||
<div className={`${styles.legendColor} ${styles.legendColorLightGreen}`}></div>
|
||||
<span className={styles.legendText}>Revenue</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BarChart data={dashboardData.chartData} />
|
||||
</div>
|
||||
{/* Chart and Transactions UI as before */}
|
||||
</div>
|
||||
|
||||
{/* Latest Transactions */}
|
||||
<div className={styles.chartCard}>
|
||||
{/* <div className={styles.chartCard}>
|
||||
<div className={styles.transactionsHeader}>
|
||||
<h3 className={styles.transactionsTitle}>Latest Transactions</h3>
|
||||
<a href="#" className={styles.seeAllLink}>see all transactions</a>
|
||||
<a href="#" className={styles.seeAllLink}>see all</a>
|
||||
</div>
|
||||
|
||||
|
||||
<div className={styles.transactionsList}>
|
||||
{dashboardData.latestTransactions.map((transaction) => (
|
||||
{products.map((transaction) => (
|
||||
<div key={transaction.id} className={styles.transactionItem}>
|
||||
<div className={styles.transactionLeft}>
|
||||
<div className={styles.transactionAvatar}>
|
||||
{transaction.avatar}
|
||||
</div>
|
||||
<div className={styles.transactionInfo}>
|
||||
<h4>{transaction.name}</h4>
|
||||
<p>on {transaction.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className={styles.transactionRight}>
|
||||
<span className={styles.transactionAmount}>
|
||||
IDR {formatCurrency(transaction.amount)}
|
||||
@@ -271,7 +252,170 @@ const Dashboard = () => {
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div> */}
|
||||
<div className={styles.chartCard}>
|
||||
<div className={styles.transactionsHeader}>
|
||||
<h3 className={styles.transactionsTitle}>Products</h3>
|
||||
</div>
|
||||
|
||||
<div className={styles.transactionsList}>
|
||||
{products.map((product) => (
|
||||
<div key={product.id} className={styles.transactionItem}>
|
||||
<div className={styles.transactionLeft}>
|
||||
<div className={styles.transactionInfo}>
|
||||
<h4>{product.name}</h4>
|
||||
{product.children && product.children.map((child) => (
|
||||
|
||||
<p>- {child.name}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.transactionRight}>
|
||||
<span className={styles.transactionAmount}>
|
||||
IDR {formatCurrency(product.amount)}
|
||||
</span>
|
||||
<div className={`${styles.statusIndicator} ${getStatusClass(product.status)}`}></div>
|
||||
<span className={styles.transactionStatus}>
|
||||
{product.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.chartCard} style={{ marginTop: '2rem' }}>
|
||||
<h3 className={styles.transactionsTitle}>Tambah Produk Baru</h3>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
const isToken = unitType === 'token';
|
||||
const durationValue = form.duration_value?.value;
|
||||
const quantityValue = form.duration_quantity?.value;
|
||||
|
||||
const dormData = {
|
||||
name: form.name.value,
|
||||
type: selectedType,
|
||||
image: form.image.value,
|
||||
description: form.description.value,
|
||||
price: parseInt(form.price.value, 10),
|
||||
currency: 'IDR',
|
||||
duration: isToken ? null : { [durationUnit]: parseInt(durationValue, 10) },
|
||||
quantity: isToken ? parseInt(quantityValue, 10) : null,
|
||||
unit_type: unitType,
|
||||
sub_product_of: null,
|
||||
is_visible: isVisible,
|
||||
group: selectedGroup,
|
||||
site_url: form.site_url.value || null,
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
sendDataToN8N('https://bot.kediritechnopark.com/webhook/store-dev/add-product', dormData);
|
||||
}}
|
||||
className={styles.form}
|
||||
>
|
||||
<div className={styles.formGroup}>
|
||||
<label>Nama Produk</label>
|
||||
<input type="text" name="name" required />
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label>Deskripsi</label>
|
||||
<textarea name="description" rows={3} required />
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label>Harga</label>
|
||||
<input type="number" name="price" required />
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label>Jenis Unit</label>
|
||||
<select
|
||||
name="unit_type"
|
||||
value={unitType}
|
||||
onChange={(e) => setUnitType(e.target.value)}
|
||||
required
|
||||
>
|
||||
<option value="duration">Durasi</option>
|
||||
<option value="token">Token</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{unitType === 'token' ? (
|
||||
<div className={styles.formGroup}>
|
||||
<label>Jumlah Token</label>
|
||||
<input type="number" name="duration_quantity" required min="1" />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.formGroup}>
|
||||
<label>Durasi</label>
|
||||
<div style={{ display: 'flex', gap: '0.5rem' }}>
|
||||
<input type="number" name="duration_value" min="1" required />
|
||||
<select name="duration_unit" value={durationUnit} onChange={(e) => setDurationUnit(e.target.value)} required>
|
||||
<option value="day">Hari</option>
|
||||
<option value="week">Minggu</option>
|
||||
<option value="month">Bulan</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label>URL Gambar</label>
|
||||
<input type="text" name="image" />
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label>Site URL (opsional)</label>
|
||||
<input type="text" name="site_url" />
|
||||
</div>
|
||||
<div className={styles.formGroup}>
|
||||
<label>Tipe Produk</label>
|
||||
<input
|
||||
type="text"
|
||||
name="type"
|
||||
value={selectedType || ''}
|
||||
onChange={(e) => setSelectedType(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<div className={styles.suggestionContainer}>
|
||||
{availableTypes.map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
className={styles.suggestionButton}
|
||||
onClick={() => setSelectedType(type)}
|
||||
>
|
||||
{type}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.formGroup}>
|
||||
<label>Group</label>
|
||||
<input
|
||||
type="text"
|
||||
name="group"
|
||||
value={selectedGroup || ''}
|
||||
onChange={(e) => setSelectedGroup(e.target.value)}
|
||||
/>
|
||||
<div className={styles.suggestionContainer}>
|
||||
{availableGroups.map((group) => (
|
||||
<button
|
||||
key={group}
|
||||
type="button"
|
||||
className={styles.suggestionButton}
|
||||
onClick={() => setSelectedGroup(group)}
|
||||
>
|
||||
{group}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" className={styles.submitButton}>Buat Produk</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user