801 lines
32 KiB
TypeScript
801 lines
32 KiB
TypeScript
import * as icon from '@mdi/js';
|
|
import Head from 'next/head';
|
|
import React, { ReactElement } from 'react';
|
|
import BaseButton from '../components/BaseButton';
|
|
import BaseIcon from '../components/BaseIcon';
|
|
import CardBox from '../components/CardBox';
|
|
import LayoutAuthenticated from '../layouts/Authenticated';
|
|
import SectionMain from '../components/SectionMain';
|
|
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
|
import { getPageTitle } from '../config';
|
|
import { useAppSelector } from '../stores/hooks';
|
|
|
|
type SynapseNode = {
|
|
id: number;
|
|
name: string;
|
|
city: string;
|
|
type: 'Creator Hub' | 'Fulfillment' | 'Retail Partner' | 'Drop Point';
|
|
health: number;
|
|
capacity: number;
|
|
latency: string;
|
|
};
|
|
|
|
type AffiliatePartner = {
|
|
id: number;
|
|
name: string;
|
|
tier: string;
|
|
sales: number;
|
|
commissionRate: number;
|
|
status: 'Aktif' | 'Butuh Follow Up' | 'Review Fraud';
|
|
};
|
|
|
|
type CreativeDrop = {
|
|
id: number;
|
|
title: string;
|
|
creator: string;
|
|
channel: string;
|
|
inventory: number;
|
|
sold: number;
|
|
margin: number;
|
|
};
|
|
|
|
type LogisticsJob = {
|
|
id: number;
|
|
code: string;
|
|
destination: string;
|
|
status: 'Routing' | 'Dikirim' | 'Terkirim' | 'Tahan QC';
|
|
eta: string;
|
|
courier: string;
|
|
risk: 'Rendah' | 'Sedang' | 'Tinggi';
|
|
};
|
|
|
|
type LedgerEvent = {
|
|
id: number;
|
|
module: string;
|
|
title: string;
|
|
detail: string;
|
|
time: string;
|
|
};
|
|
|
|
const fallbackIcon = icon.mdiViewDashboardOutline;
|
|
const resolveIcon = (name: string) =>
|
|
(name in icon ? icon[name as keyof typeof icon] : fallbackIcon) as string;
|
|
|
|
const formatCurrency = (value: number) =>
|
|
new Intl.NumberFormat('id-ID', {
|
|
style: 'currency',
|
|
currency: 'IDR',
|
|
maximumFractionDigits: 0,
|
|
}).format(value);
|
|
|
|
const formatNumber = (value: number) =>
|
|
new Intl.NumberFormat('id-ID', {
|
|
maximumFractionDigits: 0,
|
|
}).format(value);
|
|
|
|
const VortaSynapsePage = () => {
|
|
const { currentUser } = useAppSelector((state) => state.auth);
|
|
const iconsColor = useAppSelector((state) => state.style.iconsColor);
|
|
|
|
const [activeNodeId, setActiveNodeId] = React.useState(1);
|
|
const [campaignInput, setCampaignInput] = React.useState('Drop merchandise kreator lokal Batch 04');
|
|
const [selectedDropId, setSelectedDropId] = React.useState(1);
|
|
const [distributionBudget, setDistributionBudget] = React.useState('7500000');
|
|
const [commissionPool, setCommissionPool] = React.useState(18250000);
|
|
const [routingEfficiency, setRoutingEfficiency] = React.useState(86);
|
|
|
|
const [nodes, setNodes] = React.useState<SynapseNode[]>([
|
|
{
|
|
id: 1,
|
|
name: 'Jakarta Creator Hub',
|
|
city: 'Jakarta',
|
|
type: 'Creator Hub',
|
|
health: 96,
|
|
capacity: 78,
|
|
latency: '12 ms',
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'Bandung Micro Fulfillment',
|
|
city: 'Bandung',
|
|
type: 'Fulfillment',
|
|
health: 91,
|
|
capacity: 64,
|
|
latency: '18 ms',
|
|
},
|
|
{
|
|
id: 3,
|
|
name: 'Surabaya Affiliate Gate',
|
|
city: 'Surabaya',
|
|
type: 'Retail Partner',
|
|
health: 88,
|
|
capacity: 72,
|
|
latency: '24 ms',
|
|
},
|
|
{
|
|
id: 4,
|
|
name: 'Bali Pop-Up Drop Point',
|
|
city: 'Denpasar',
|
|
type: 'Drop Point',
|
|
health: 83,
|
|
capacity: 55,
|
|
latency: '31 ms',
|
|
},
|
|
]);
|
|
|
|
const [partners, setPartners] = React.useState<AffiliatePartner[]>([
|
|
{
|
|
id: 1,
|
|
name: 'Nara Studio Circle',
|
|
tier: 'Creator Lead',
|
|
sales: 146,
|
|
commissionRate: 14,
|
|
status: 'Aktif',
|
|
},
|
|
{
|
|
id: 2,
|
|
name: 'Kolektif Musik Indie',
|
|
tier: 'Community Partner',
|
|
sales: 92,
|
|
commissionRate: 11,
|
|
status: 'Aktif',
|
|
},
|
|
{
|
|
id: 3,
|
|
name: 'Micro Seller Timur',
|
|
tier: 'Regional Affiliate',
|
|
sales: 51,
|
|
commissionRate: 9,
|
|
status: 'Butuh Follow Up',
|
|
},
|
|
{
|
|
id: 4,
|
|
name: 'Promo Flash Network',
|
|
tier: 'Performance',
|
|
sales: 18,
|
|
commissionRate: 7,
|
|
status: 'Review Fraud',
|
|
},
|
|
]);
|
|
|
|
const [drops, setDrops] = React.useState<CreativeDrop[]>([
|
|
{
|
|
id: 1,
|
|
title: 'Hoodie Tur Konser Virtual',
|
|
creator: 'Aksara Nada',
|
|
channel: 'TikTok Shop + Komunitas',
|
|
inventory: 420,
|
|
sold: 287,
|
|
margin: 38,
|
|
},
|
|
{
|
|
id: 2,
|
|
title: 'Print Art Edisi 04',
|
|
creator: 'Maya Visual Lab',
|
|
channel: 'Marketplace Kolektor',
|
|
inventory: 180,
|
|
sold: 119,
|
|
margin: 44,
|
|
},
|
|
{
|
|
id: 3,
|
|
title: 'Paket Kelas Mini Creator',
|
|
creator: 'Ruang Cerita',
|
|
channel: 'Affiliate Academy',
|
|
inventory: 650,
|
|
sold: 312,
|
|
margin: 52,
|
|
},
|
|
]);
|
|
|
|
const [jobs, setJobs] = React.useState<LogisticsJob[]>([
|
|
{
|
|
id: 1,
|
|
code: 'VSP-0401',
|
|
destination: 'Jakarta Selatan',
|
|
status: 'Routing',
|
|
eta: '2 jam',
|
|
courier: 'Vorta Route AI',
|
|
risk: 'Rendah',
|
|
},
|
|
{
|
|
id: 2,
|
|
code: 'VSP-0402',
|
|
destination: 'Bandung Kota',
|
|
status: 'Dikirim',
|
|
eta: '5 jam',
|
|
courier: 'Synapse Express',
|
|
risk: 'Sedang',
|
|
},
|
|
{
|
|
id: 3,
|
|
code: 'VSP-0403',
|
|
destination: 'Surabaya Timur',
|
|
status: 'Tahan QC',
|
|
eta: '12 jam',
|
|
courier: 'Partner Fleet',
|
|
risk: 'Tinggi',
|
|
},
|
|
]);
|
|
|
|
const [ledger, setLedger] = React.useState<LedgerEvent[]>([
|
|
{
|
|
id: 1,
|
|
module: 'Distribusi',
|
|
title: 'Batch 04 disinkronkan',
|
|
detail: '287 unit hoodie dialokasikan dari Jakarta Creator Hub ke 3 node regional.',
|
|
time: 'Baru saja',
|
|
},
|
|
{
|
|
id: 2,
|
|
module: 'Afiliasi',
|
|
title: 'Komisi kreator dihitung',
|
|
detail: 'Pool komisi minggu ini siap dibagi ke 4 partner aktif.',
|
|
time: '8 menit lalu',
|
|
},
|
|
{
|
|
id: 3,
|
|
module: 'Logistik',
|
|
title: 'Rute risiko tinggi ditahan',
|
|
detail: 'Paket VSP-0403 masuk antrian QC sebelum dilanjutkan ke Surabaya Timur.',
|
|
time: '14 menit lalu',
|
|
},
|
|
]);
|
|
|
|
const userName = currentUser?.firstName || currentUser?.email || 'Operator Vorta';
|
|
const activeNode = nodes.find((node) => node.id === activeNodeId) || nodes[0];
|
|
const selectedDrop = drops.find((drop) => drop.id === selectedDropId) || drops[0];
|
|
const totalInventory = drops.reduce((sum, drop) => sum + drop.inventory, 0);
|
|
const totalSold = drops.reduce((sum, drop) => sum + drop.sold, 0);
|
|
const sellThrough = Math.round((totalSold / totalInventory) * 100);
|
|
const partnerSales = partners.reduce((sum, partner) => sum + partner.sales, 0);
|
|
const averageCommission = Math.round(
|
|
partners.reduce((sum, partner) => sum + partner.commissionRate, 0) / partners.length,
|
|
);
|
|
const openJobs = jobs.filter((job) => job.status !== 'Terkirim').length;
|
|
|
|
const addLedgerEvent = (module: string, title: string, detail: string) => {
|
|
setLedger((current) => [
|
|
{
|
|
id: Date.now(),
|
|
module,
|
|
title,
|
|
detail,
|
|
time: 'Baru saja',
|
|
},
|
|
...current.slice(0, 6),
|
|
]);
|
|
};
|
|
|
|
const launchCampaign = () => {
|
|
const trimmedCampaign = campaignInput.trim();
|
|
const budget = Number(distributionBudget);
|
|
|
|
if (!trimmedCampaign || !budget || budget <= 0) {
|
|
return;
|
|
}
|
|
|
|
const nextDrop: CreativeDrop = {
|
|
id: Date.now(),
|
|
title: trimmedCampaign,
|
|
creator: String(userName),
|
|
channel: `${activeNode.city} Synapse Node`,
|
|
inventory: Math.max(80, Math.round(budget / 50000)),
|
|
sold: 0,
|
|
margin: selectedDrop.margin,
|
|
};
|
|
|
|
setDrops((current) => [nextDrop, ...current]);
|
|
setSelectedDropId(nextDrop.id);
|
|
setCommissionPool((current) => current + Math.round(budget * 0.18));
|
|
setCampaignInput('');
|
|
setDistributionBudget('');
|
|
addLedgerEvent(
|
|
'Distribusi',
|
|
'Kampanye kreator diluncurkan',
|
|
`${nextDrop.title} masuk node ${activeNode.name} dengan estimasi stok ${formatNumber(nextDrop.inventory)} unit.`,
|
|
);
|
|
};
|
|
|
|
const optimizeRoute = () => {
|
|
setRoutingEfficiency((current) => Math.min(99, current + 3));
|
|
setNodes((current) =>
|
|
current.map((node) =>
|
|
node.id === activeNodeId
|
|
? {
|
|
...node,
|
|
health: Math.min(99, node.health + 2),
|
|
capacity: Math.max(35, node.capacity - 4),
|
|
}
|
|
: node,
|
|
),
|
|
);
|
|
setJobs((current) =>
|
|
current.map((job, index) =>
|
|
index === 0
|
|
? {
|
|
...job,
|
|
status: 'Dikirim',
|
|
eta: '90 menit',
|
|
risk: 'Rendah',
|
|
}
|
|
: job,
|
|
),
|
|
);
|
|
addLedgerEvent(
|
|
'Logistik',
|
|
'Rute pintar dioptimalkan',
|
|
`${activeNode.name} menurunkan ETA prioritas dan meningkatkan efisiensi rute menjadi ${Math.min(99, routingEfficiency + 3)}%.`,
|
|
);
|
|
};
|
|
|
|
const settleCommissions = () => {
|
|
if (commissionPool <= 0) {
|
|
return;
|
|
}
|
|
|
|
setPartners((current) =>
|
|
current.map((partner) => ({
|
|
...partner,
|
|
sales: partner.sales + Math.round(partner.sales * 0.06),
|
|
status: partner.status === 'Review Fraud' ? 'Butuh Follow Up' : 'Aktif',
|
|
})),
|
|
);
|
|
addLedgerEvent(
|
|
'Afiliasi',
|
|
'Settlement komisi diproses',
|
|
`${formatCurrency(commissionPool)} dialokasikan ke partner dengan rata-rata komisi ${averageCommission}%.`,
|
|
);
|
|
setCommissionPool(0);
|
|
};
|
|
|
|
const dispatchShipment = (jobId: number) => {
|
|
setJobs((current) =>
|
|
current.map((job) =>
|
|
job.id === jobId
|
|
? {
|
|
...job,
|
|
status: job.status === 'Terkirim' ? 'Terkirim' : 'Terkirim',
|
|
eta: 'Selesai',
|
|
risk: 'Rendah',
|
|
}
|
|
: job,
|
|
),
|
|
);
|
|
const completedJob = jobs.find((job) => job.id === jobId);
|
|
|
|
if (completedJob) {
|
|
addLedgerEvent(
|
|
'Logistik',
|
|
'Paket diselesaikan',
|
|
`${completedJob.code} ke ${completedJob.destination} ditandai terkirim dan siap settlement kreator.`,
|
|
);
|
|
}
|
|
};
|
|
|
|
const boostSelectedDrop = () => {
|
|
setDrops((current) =>
|
|
current.map((drop) =>
|
|
drop.id === selectedDropId
|
|
? {
|
|
...drop,
|
|
sold: Math.min(drop.inventory, drop.sold + 24),
|
|
margin: Math.min(70, drop.margin + 1),
|
|
}
|
|
: drop,
|
|
),
|
|
);
|
|
addLedgerEvent(
|
|
'Distribusi',
|
|
'Afiliasi boost diterapkan',
|
|
`${selectedDrop.title} mendapat 24 proyeksi penjualan baru lewat partner performa tinggi.`,
|
|
);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>{getPageTitle('04 Vorta Synapse')}</title>
|
|
</Head>
|
|
<SectionMain>
|
|
<SectionTitleLineWithButton
|
|
icon={resolveIcon('mdiTransitConnectionVariant')}
|
|
title="04 Vorta Synapse"
|
|
main
|
|
>
|
|
<BaseButton
|
|
href="/vorta-commerce"
|
|
icon={icon.mdiViewDashboardOutline}
|
|
label="Kembali ke Dasbor"
|
|
color="whiteDark"
|
|
/>
|
|
</SectionTitleLineWithButton>
|
|
|
|
<CardBox className="mb-6 overflow-hidden" hasComponentLayout>
|
|
<div className="grid grid-cols-1 gap-0 xl:grid-cols-5">
|
|
<div className="relative overflow-hidden p-6 xl:col-span-3">
|
|
<div className="absolute right-0 top-0 h-48 w-48 rounded-full bg-blue-200/30 blur-3xl dark:bg-blue-900/30" />
|
|
<div className="absolute bottom-0 right-28 h-36 w-36 rounded-full bg-green-200/30 blur-3xl dark:bg-green-900/30" />
|
|
<div className="relative">
|
|
<div className="mb-3 inline-flex items-center rounded-full bg-blue-50 px-3 py-1 text-sm font-semibold text-blue-700 dark:bg-blue-900/30 dark:text-blue-200">
|
|
<BaseIcon path={resolveIcon('mdiNetworkOutline')} className="mr-2" size={18} />
|
|
Protokol distribusi, afiliasi, dan logistik pintar untuk ekonomi kreator
|
|
</div>
|
|
<h2 className="mb-3 text-3xl font-bold leading-tight md:text-4xl">
|
|
Halo {userName}, sinkronkan drop kreator dari kampanye sampai paket terkirim.
|
|
</h2>
|
|
<p className="mb-5 max-w-3xl text-gray-600 dark:text-gray-300">
|
|
Vorta Synapse menyatukan node distribusi, partner afiliasi, pool komisi,
|
|
alokasi stok, dan rute logistik adaptif agar kreator bisa menjual lebih luas
|
|
tanpa kehilangan kontrol operasional.
|
|
</p>
|
|
<div className="grid grid-cols-2 gap-3 md:grid-cols-4">
|
|
{[
|
|
{ label: 'Sell-through', value: `${sellThrough}%`, iconName: 'mdiChartTimelineVariant' },
|
|
{ label: 'Penjualan afiliasi', value: formatNumber(partnerSales), iconName: 'mdiAccountMultipleCheckOutline' },
|
|
{ label: 'Komisi siap', value: formatCurrency(commissionPool), iconName: 'mdiCashSync' },
|
|
{ label: 'Job logistik', value: openJobs, iconName: 'mdiTruckFastOutline' },
|
|
].map((item) => (
|
|
<div
|
|
key={item.label}
|
|
className="rounded-2xl border border-gray-100 bg-white/70 p-4 shadow-sm dark:border-dark-700 dark:bg-dark-800/60"
|
|
>
|
|
<BaseIcon path={resolveIcon(item.iconName)} className={`${iconsColor} mb-3`} size={26} />
|
|
<div className="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
|
{item.label}
|
|
</div>
|
|
<div className="mt-1 text-xl font-bold">{item.value}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="border-t border-gray-100 bg-gray-50 p-6 dark:border-dark-700 dark:bg-dark-800/70 xl:col-span-2 xl:border-l xl:border-t-0">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Synapse health</p>
|
|
<h3 className="text-xl font-semibold">{routingEfficiency}% route efficiency</h3>
|
|
</div>
|
|
<span className="rounded-full bg-green-100 px-3 py-1 text-sm font-semibold text-green-700 dark:bg-green-900/30 dark:text-green-200">
|
|
Protocol Live
|
|
</span>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{ledger.slice(0, 4).map((event) => (
|
|
<div key={event.id} className="rounded-xl bg-white p-3 dark:bg-dark-900">
|
|
<div className="mb-1 flex items-center justify-between text-sm">
|
|
<span className="font-semibold text-blue-700 dark:text-blue-300">{event.module}</span>
|
|
<span className="text-gray-400">{event.time}</span>
|
|
</div>
|
|
<p className="font-semibold">{event.title}</p>
|
|
<p className="text-sm text-gray-600 dark:text-gray-300">{event.detail}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardBox>
|
|
|
|
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
|
|
<CardBox className="xl:col-span-2" hasComponentLayout>
|
|
<div className="grid min-h-[650px] grid-cols-1 lg:grid-cols-5">
|
|
<aside className="border-b border-gray-100 p-4 dark:border-dark-700 lg:col-span-2 lg:border-b-0 lg:border-r">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-xl font-semibold">Synapse Nodes</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
Pusat kreator, fulfillment, partner retail, dan drop point.
|
|
</p>
|
|
</div>
|
|
<BaseIcon path={resolveIcon('mdiAccessPointNetwork')} className={iconsColor} size={30} />
|
|
</div>
|
|
<div className="space-y-3">
|
|
{nodes.map((node) => (
|
|
<button
|
|
key={node.id}
|
|
type="button"
|
|
onClick={() => setActiveNodeId(node.id)}
|
|
className={`w-full rounded-2xl border p-3 text-left transition ${
|
|
activeNodeId === node.id
|
|
? 'border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20'
|
|
: 'border-gray-100 bg-white hover:border-blue-200 dark:border-dark-700 dark:bg-dark-900'
|
|
}`}
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-semibold">{node.name}</span>
|
|
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-xs font-bold text-blue-700 dark:bg-blue-900/30 dark:text-blue-200">
|
|
{node.health}%
|
|
</span>
|
|
</div>
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
{node.city} • {node.type} • Latensi {node.latency}
|
|
</p>
|
|
<div className="mt-3 h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-dark-800">
|
|
<div className="h-full rounded-full bg-blue-500" style={{ width: `${node.capacity}%` }} />
|
|
</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</aside>
|
|
|
|
<section className="flex flex-col lg:col-span-3">
|
|
<div className="border-b border-gray-100 p-4 dark:border-dark-700">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Node aktif</p>
|
|
<div className="flex flex-col justify-between gap-3 md:flex-row md:items-center">
|
|
<div>
|
|
<h3 className="text-2xl font-semibold">{activeNode.name}</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
{activeNode.type} di {activeNode.city} • kapasitas {activeNode.capacity}% terpakai
|
|
</p>
|
|
</div>
|
|
<BaseButton
|
|
label="Optimalkan Rute"
|
|
icon={resolveIcon('mdiRoutes')}
|
|
color="info"
|
|
onClick={optimizeRoute}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="flex-1 bg-gray-50 p-4 dark:bg-dark-800/50">
|
|
<div className="grid grid-cols-1 gap-4 md:grid-cols-3">
|
|
<div className="rounded-3xl bg-white p-4 dark:bg-dark-900">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Health score</p>
|
|
<div className="mt-2 text-4xl font-bold text-green-600">{activeNode.health}%</div>
|
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
Node siap menerima batch dan menjaga SLA fulfillment.
|
|
</p>
|
|
</div>
|
|
<div className="rounded-3xl bg-white p-4 dark:bg-dark-900">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Routing efficiency</p>
|
|
<div className="mt-2 text-4xl font-bold text-blue-600">{routingEfficiency}%</div>
|
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
Optimasi mempertimbangkan stok, risiko, jarak, dan kapasitas mitra.
|
|
</p>
|
|
</div>
|
|
<div className="rounded-3xl bg-white p-4 dark:bg-dark-900">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Average commission</p>
|
|
<div className="mt-2 text-4xl font-bold text-orange-500">{averageCommission}%</div>
|
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
Tarif dinamis mengikuti performa dan kualitas traffic partner.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-4 rounded-3xl bg-white p-4 dark:bg-dark-900">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<div>
|
|
<h4 className="font-semibold">Launcher kampanye kreator</h4>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
Buat batch distribusi baru dan isi pool afiliasi otomatis.
|
|
</p>
|
|
</div>
|
|
<BaseIcon path={resolveIcon('mdiRocketLaunchOutline')} className={iconsColor} size={28} />
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-3 md:grid-cols-3">
|
|
<input
|
|
value={campaignInput}
|
|
onChange={(event) => setCampaignInput(event.target.value)}
|
|
className="h-11 rounded-xl border border-gray-200 bg-white px-3 text-sm outline-none focus:border-blue-400 focus:ring dark:border-dark-700 dark:bg-dark-800 md:col-span-2"
|
|
placeholder="Nama drop atau kampanye kreator"
|
|
/>
|
|
<input
|
|
value={distributionBudget}
|
|
onChange={(event) => setDistributionBudget(event.target.value)}
|
|
className="h-11 rounded-xl border border-gray-200 bg-white px-3 text-sm outline-none focus:border-blue-400 focus:ring dark:border-dark-700 dark:bg-dark-800"
|
|
placeholder="Budget distribusi"
|
|
inputMode="numeric"
|
|
/>
|
|
</div>
|
|
<div className="mt-3 flex flex-wrap items-center justify-between gap-3">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
Template margin mengikuti {selectedDrop.title} ({selectedDrop.margin}%).
|
|
</p>
|
|
<BaseButton
|
|
label="Luncurkan Batch"
|
|
icon={resolveIcon('mdiPlusCircleOutline')}
|
|
color="success"
|
|
onClick={launchCampaign}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</CardBox>
|
|
|
|
<div className="space-y-6">
|
|
<CardBox hasComponentLayout>
|
|
<div className="p-5">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-xl font-semibold">Komisi Afiliasi</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
Pool komisi untuk kreator, komunitas, dan reseller.
|
|
</p>
|
|
</div>
|
|
<BaseIcon path={resolveIcon('mdiCashMultiple')} className={iconsColor} size={32} />
|
|
</div>
|
|
<div className="rounded-3xl bg-gray-50 p-4 dark:bg-dark-800">
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">Pool siap settlement</p>
|
|
<div className="mt-1 text-3xl font-bold">{formatCurrency(commissionPool)}</div>
|
|
<p className="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
|
Partner aktif: {partners.filter((partner) => partner.status === 'Aktif').length}/{partners.length}
|
|
</p>
|
|
<BaseButton
|
|
label="Settlement Komisi"
|
|
icon={resolveIcon('mdiBankTransferOut')}
|
|
color="info"
|
|
disabled={commissionPool <= 0}
|
|
className="mt-3 w-full"
|
|
onClick={settleCommissions}
|
|
/>
|
|
</div>
|
|
<div className="mt-4 space-y-3">
|
|
{partners.map((partner) => (
|
|
<div key={partner.id} className="rounded-2xl border border-gray-100 p-3 dark:border-dark-700">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div>
|
|
<p className="font-semibold">{partner.name}</p>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">{partner.tier}</p>
|
|
</div>
|
|
<span
|
|
className={`rounded-full px-2 py-1 text-xs font-bold ${
|
|
partner.status === 'Aktif'
|
|
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-200'
|
|
: partner.status === 'Review Fraud'
|
|
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-200'
|
|
: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-200'
|
|
}`}
|
|
>
|
|
{partner.status}
|
|
</span>
|
|
</div>
|
|
<div className="mt-3 grid grid-cols-2 gap-2 text-sm">
|
|
<div className="rounded-xl bg-gray-50 p-2 dark:bg-dark-800">
|
|
<span className="text-gray-500 dark:text-gray-400">Sales</span>
|
|
<p className="font-semibold">{formatNumber(partner.sales)}</p>
|
|
</div>
|
|
<div className="rounded-xl bg-gray-50 p-2 dark:bg-dark-800">
|
|
<span className="text-gray-500 dark:text-gray-400">Rate</span>
|
|
<p className="font-semibold">{partner.commissionRate}%</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardBox>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mt-6 grid grid-cols-1 gap-6 xl:grid-cols-2">
|
|
<CardBox hasComponentLayout>
|
|
<div className="p-5">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-xl font-semibold">Drop Kreator</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
Produk digital/fisik yang sedang didorong oleh protokol distribusi.
|
|
</p>
|
|
</div>
|
|
<BaseIcon path={resolveIcon('mdiPackageVariantClosed')} className={iconsColor} size={32} />
|
|
</div>
|
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3 xl:grid-cols-1 2xl:grid-cols-3">
|
|
{drops.map((drop) => {
|
|
const progress = Math.round((drop.sold / drop.inventory) * 100);
|
|
|
|
return (
|
|
<button
|
|
key={drop.id}
|
|
type="button"
|
|
onClick={() => setSelectedDropId(drop.id)}
|
|
className={`rounded-2xl border p-4 text-left transition ${
|
|
selectedDropId === drop.id
|
|
? 'border-blue-300 bg-blue-50 dark:border-blue-800 dark:bg-blue-900/20'
|
|
: 'border-gray-100 hover:border-blue-200 dark:border-dark-700'
|
|
}`}
|
|
>
|
|
<span className="mb-3 inline-flex rounded-full bg-orange-50 px-3 py-1 text-xs font-bold text-orange-700 dark:bg-orange-900/30 dark:text-orange-200">
|
|
Margin {drop.margin}%
|
|
</span>
|
|
<h4 className="min-h-[48px] font-semibold">{drop.title}</h4>
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
{drop.creator} • {drop.channel}
|
|
</p>
|
|
<div className="mt-3 flex items-center justify-between text-sm">
|
|
<span>{formatNumber(drop.sold)} / {formatNumber(drop.inventory)} terjual</span>
|
|
<span className="font-semibold">{progress}%</span>
|
|
</div>
|
|
<div className="mt-2 h-2 overflow-hidden rounded-full bg-gray-100 dark:bg-dark-800">
|
|
<div className="h-full rounded-full bg-green-500" style={{ width: `${progress}%` }} />
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
<div className="mt-5 rounded-3xl bg-gray-50 p-4 dark:bg-dark-800">
|
|
<div className="mb-3 flex items-center justify-between">
|
|
<div>
|
|
<h4 className="font-semibold">Boost afiliasi terpilih</h4>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">{selectedDrop.title}</p>
|
|
</div>
|
|
<BaseButton
|
|
label="Boost 24 Sales"
|
|
icon={resolveIcon('mdiTrendingUp')}
|
|
color="success"
|
|
small
|
|
onClick={boostSelectedDrop}
|
|
/>
|
|
</div>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
Simulasi ini menambah proyeksi penjualan dan menaikkan margin drop pilihan.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CardBox>
|
|
|
|
<CardBox hasComponentLayout>
|
|
<div className="p-5">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-xl font-semibold">Logistik Pintar</h3>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
Queue pengiriman dengan ETA, risiko, dan aksi penyelesaian.
|
|
</p>
|
|
</div>
|
|
<BaseIcon path={resolveIcon('mdiTruckDeliveryOutline')} className={iconsColor} size={32} />
|
|
</div>
|
|
<div className="space-y-4">
|
|
{jobs.map((job) => (
|
|
<div key={job.id} className="rounded-2xl border border-gray-100 p-4 dark:border-dark-700">
|
|
<div className="flex flex-col justify-between gap-3 md:flex-row md:items-start">
|
|
<div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<h4 className="font-semibold">{job.code}</h4>
|
|
<span
|
|
className={`rounded-full px-2 py-1 text-xs font-bold ${
|
|
job.risk === 'Rendah'
|
|
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-200'
|
|
: job.risk === 'Tinggi'
|
|
? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-200'
|
|
: 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-200'
|
|
}`}
|
|
>
|
|
Risiko {job.risk}
|
|
</span>
|
|
</div>
|
|
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
{job.destination} • {job.courier}
|
|
</p>
|
|
</div>
|
|
<div className="text-left md:text-right">
|
|
<p className="font-semibold">{job.status}</p>
|
|
<p className="text-sm text-gray-500 dark:text-gray-400">ETA {job.eta}</p>
|
|
</div>
|
|
</div>
|
|
<BaseButton
|
|
label={job.status === 'Terkirim' ? 'Sudah Terkirim' : 'Tandai Terkirim'}
|
|
icon={resolveIcon('mdiCheckCircleOutline')}
|
|
color={job.status === 'Terkirim' ? 'success' : 'info'}
|
|
small
|
|
disabled={job.status === 'Terkirim'}
|
|
className="mt-3 w-full"
|
|
onClick={() => dispatchShipment(job.id)}
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</CardBox>
|
|
</div>
|
|
</SectionMain>
|
|
</>
|
|
);
|
|
};
|
|
|
|
VortaSynapsePage.getLayout = function getLayout(page: ReactElement) {
|
|
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
|
};
|
|
|
|
export default VortaSynapsePage;
|