diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx
index da31d39..edf0ddf 100644
--- a/frontend/src/components/AsideMenuLayer.tsx
+++ b/frontend/src/components/AsideMenuLayer.tsx
@@ -3,10 +3,9 @@ import { mdiLogout, mdiClose } from '@mdi/js'
import BaseIcon from './BaseIcon'
import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces'
-import { useAppSelector } from '../stores/hooks'
+import { useAppDispatch, useAppSelector } from '../stores/hooks'
import Link from 'next/link';
-import { useAppDispatch } from '../stores/hooks';
import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
@@ -68,7 +67,7 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
>
-
Vorta Universe MVP
+
VORTA-COMMERCE
{organizationName &&
{organizationName}
}
diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx
index eb155e3..fb0fca2 100644
--- a/frontend/src/components/NavBarItem.tsx
+++ b/frontend/src/components/NavBarItem.tsx
@@ -1,6 +1,5 @@
-import React, {useEffect, useRef} from 'react'
+import React, { useEffect, useRef, useState } from 'react'
import Link from 'next/link'
-import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'
diff --git a/frontend/src/config.ts b/frontend/src/config.ts
index a9783c8..3424a08 100644
--- a/frontend/src/config.ts
+++ b/frontend/src/config.ts
@@ -8,7 +8,7 @@ export const localStorageStyleKey = 'style'
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
-export const appTitle = 'created by Flatlogic generator!'
+export const appTitle = 'Ekosistem Kepercayaan Digital'
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx
index 1b9907d..73d8391 100644
--- a/frontend/src/layouts/Authenticated.tsx
+++ b/frontend/src/layouts/Authenticated.tsx
@@ -1,5 +1,4 @@
-import React, { ReactNode, useEffect } from 'react'
-import { useState } from 'react'
+import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index 0b796bc..4898aad 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -1,355 +1,487 @@
import * as icon from '@mdi/js';
-import { MenuAsideItem } from './interfaces'
+import { MenuAsideItem } from './interfaces';
const menuAside: MenuAsideItem[] = [
{
- href: '/dashboard',
- icon: icon.mdiViewDashboardOutline,
- label: 'Dashboard',
+ href: '/vorta-commerce',
+ icon:
+ 'mdiStorefrontOutline' in icon
+ ? (icon['mdiStorefrontOutline' as keyof typeof icon] as string)
+ : icon.mdiViewDashboardOutline,
+ label: 'VORTA-Commerce',
},
-
+
+ {
+ href: '/mega-super-app',
+ label: 'Mega Super-App',
+ icon:
+ 'mdiApps' in icon
+ ? (icon['mdiApps' as keyof typeof icon] as string)
+ : icon.mdiViewDashboardOutline,
+ },
+
+
+ {
+ href: '/vorta-synapse',
+ label: '04 Vorta Synapse',
+ icon:
+ 'mdiTransitConnectionVariant' in icon
+ ? (icon['mdiTransitConnectionVariant' as keyof typeof icon] as string)
+ : icon.mdiViewDashboardOutline,
+ },
+
{
href: '/users/users-list',
- label: 'Users',
+ label: 'Pengguna',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiAccountGroup ?? icon.mdiTable,
- permissions: 'READ_USERS'
+ permissions: 'READ_USERS',
},
{
href: '/roles/roles-list',
- label: 'Roles',
+ label: 'Peran',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountVariantOutline ?? icon.mdiTable,
- permissions: 'READ_ROLES'
+ permissions: 'READ_ROLES',
},
{
href: '/permissions/permissions-list',
- label: 'Permissions',
+ label: 'Izin Akses',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiShieldAccountOutline ?? icon.mdiTable,
- permissions: 'READ_PERMISSIONS'
+ permissions: 'READ_PERMISSIONS',
},
{
href: '/organizations/organizations-list',
- label: 'Organizations',
+ label: 'Organisasi',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
icon: icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_ORGANIZATIONS'
+ permissions: 'READ_ORGANIZATIONS',
},
{
href: '/trust_profiles/trust_profiles-list',
- label: 'Trust profiles',
+ label: 'Profil Kepercayaan',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiShieldCheck' in icon ? icon['mdiShieldCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_TRUST_PROFILES'
+ icon:
+ 'mdiShieldCheck' in icon
+ ? icon['mdiShieldCheck' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_TRUST_PROFILES',
},
{
href: '/identity_verifications/identity_verifications-list',
- label: 'Identity verifications',
+ label: 'Verifikasi Identitas',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiCardAccountDetailsOutline' in icon ? icon['mdiCardAccountDetailsOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_IDENTITY_VERIFICATIONS'
+ icon:
+ 'mdiCardAccountDetailsOutline' in icon
+ ? icon['mdiCardAccountDetailsOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_IDENTITY_VERIFICATIONS',
},
{
href: '/wallets/wallets-list',
- label: 'Wallets',
+ label: 'Dompet',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiWallet' in icon ? icon['mdiWallet' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_WALLETS'
+ icon:
+ 'mdiWallet' in icon
+ ? icon['mdiWallet' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_WALLETS',
},
{
href: '/wallet_transactions/wallet_transactions-list',
- label: 'Wallet transactions',
+ label: 'Transaksi Dompet',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiSwapHorizontal' in icon ? icon['mdiSwapHorizontal' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_WALLET_TRANSACTIONS'
+ icon:
+ 'mdiSwapHorizontal' in icon
+ ? icon['mdiSwapHorizontal' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_WALLET_TRANSACTIONS',
},
{
href: '/chats/chats-list',
- label: 'Chats',
+ label: 'Percakapan',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiChat' in icon ? icon['mdiChat' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_CHATS'
+ icon:
+ 'mdiChat' in icon
+ ? icon['mdiChat' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_CHATS',
},
{
href: '/chat_participants/chat_participants-list',
- label: 'Chat participants',
+ label: 'Peserta Percakapan',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiAccountGroup' in icon ? icon['mdiAccountGroup' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_CHAT_PARTICIPANTS'
+ icon:
+ 'mdiAccountGroup' in icon
+ ? icon['mdiAccountGroup' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_CHAT_PARTICIPANTS',
},
{
href: '/messages/messages-list',
- label: 'Messages',
+ label: 'Pesan',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiMessageTextOutline' in icon ? icon['mdiMessageTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_MESSAGES'
+ icon:
+ 'mdiMessageTextOutline' in icon
+ ? icon['mdiMessageTextOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_MESSAGES',
},
{
href: '/stores/stores-list',
- label: 'Stores',
+ label: 'Toko',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiStore' in icon ? icon['mdiStore' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_STORES'
+ icon:
+ 'mdiStore' in icon
+ ? icon['mdiStore' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_STORES',
},
{
href: '/addresses/addresses-list',
- label: 'Addresses',
+ label: 'Alamat',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiMapMarker' in icon ? icon['mdiMapMarker' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_ADDRESSES'
+ icon:
+ 'mdiMapMarker' in icon
+ ? icon['mdiMapMarker' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_ADDRESSES',
},
{
href: '/product_categories/product_categories-list',
- label: 'Product categories',
+ label: 'Kategori Produk',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiShapeOutline' in icon ? icon['mdiShapeOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_PRODUCT_CATEGORIES'
+ icon:
+ 'mdiShapeOutline' in icon
+ ? icon['mdiShapeOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_PRODUCT_CATEGORIES',
},
{
href: '/products/products-list',
- label: 'Products',
+ label: 'Produk',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiPackageVariantClosed' in icon ? icon['mdiPackageVariantClosed' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_PRODUCTS'
+ icon:
+ 'mdiPackageVariantClosed' in icon
+ ? icon['mdiPackageVariantClosed' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_PRODUCTS',
},
{
href: '/orders/orders-list',
- label: 'Orders',
+ label: 'Pesanan',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiReceiptTextOutline' in icon ? icon['mdiReceiptTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_ORDERS'
+ icon:
+ 'mdiReceiptTextOutline' in icon
+ ? icon['mdiReceiptTextOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_ORDERS',
},
{
href: '/order_items/order_items-list',
- label: 'Order items',
+ label: 'Item Pesanan',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiFormatListBulleted' in icon ? icon['mdiFormatListBulleted' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_ORDER_ITEMS'
+ icon:
+ 'mdiFormatListBulleted' in icon
+ ? icon['mdiFormatListBulleted' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_ORDER_ITEMS',
},
{
href: '/payments/payments-list',
- label: 'Payments',
+ label: 'Pembayaran',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiCreditCardOutline' in icon ? icon['mdiCreditCardOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_PAYMENTS'
+ icon:
+ 'mdiCreditCardOutline' in icon
+ ? icon['mdiCreditCardOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_PAYMENTS',
},
{
href: '/product_reviews/product_reviews-list',
- label: 'Product reviews',
+ label: 'Ulasan Produk',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiStarCircleOutline' in icon ? icon['mdiStarCircleOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_PRODUCT_REVIEWS'
+ icon:
+ 'mdiStarCircleOutline' in icon
+ ? icon['mdiStarCircleOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_PRODUCT_REVIEWS',
},
{
href: '/shipments/shipments-list',
- label: 'Shipments',
+ label: 'Pengiriman',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiTruckDeliveryOutline' in icon ? icon['mdiTruckDeliveryOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_SHIPMENTS'
+ icon:
+ 'mdiTruckDeliveryOutline' in icon
+ ? icon['mdiTruckDeliveryOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_SHIPMENTS',
},
{
href: '/posts/posts-list',
- label: 'Posts',
+ label: 'Postingan',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiPlayCircleOutline' in icon ? icon['mdiPlayCircleOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_POSTS'
+ icon:
+ 'mdiPlayCircleOutline' in icon
+ ? icon['mdiPlayCircleOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_POSTS',
},
{
href: '/deep_dive_links/deep_dive_links-list',
- label: 'Deep dive links',
+ label: 'Tautan Deep Dive',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiLinkVariant' in icon ? icon['mdiLinkVariant' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_DEEP_DIVE_LINKS'
+ icon:
+ 'mdiLinkVariant' in icon
+ ? icon['mdiLinkVariant' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_DEEP_DIVE_LINKS',
},
{
href: '/citations/citations-list',
- label: 'Citations',
+ label: 'Sitasi',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiFileDocumentCheckOutline' in icon ? icon['mdiFileDocumentCheckOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_CITATIONS'
+ icon:
+ 'mdiFileDocumentCheckOutline' in icon
+ ? icon['mdiFileDocumentCheckOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_CITATIONS',
},
{
href: '/post_engagements/post_engagements-list',
- label: 'Post engagements',
+ label: 'Interaksi Postingan',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiThumbUpOutline' in icon ? icon['mdiThumbUpOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_POST_ENGAGEMENTS'
+ icon:
+ 'mdiThumbUpOutline' in icon
+ ? icon['mdiThumbUpOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_POST_ENGAGEMENTS',
},
{
href: '/rewards/rewards-list',
- label: 'Rewards',
+ label: 'Hadiah',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiTrophyOutline' in icon ? icon['mdiTrophyOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_REWARDS'
+ icon:
+ 'mdiTrophyOutline' in icon
+ ? icon['mdiTrophyOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_REWARDS',
},
{
href: '/forum_spaces/forum_spaces-list',
- label: 'Forum spaces',
+ label: 'Ruang Forum',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiForumOutline' in icon ? icon['mdiForumOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_FORUM_SPACES'
+ icon:
+ 'mdiForumOutline' in icon
+ ? icon['mdiForumOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_FORUM_SPACES',
},
{
href: '/forum_threads/forum_threads-list',
- label: 'Forum threads',
+ label: 'Topik Forum',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiMessageBulleted' in icon ? icon['mdiMessageBulleted' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_FORUM_THREADS'
+ icon:
+ 'mdiMessageBulleted' in icon
+ ? icon['mdiMessageBulleted' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_FORUM_THREADS',
},
{
href: '/forum_posts/forum_posts-list',
- label: 'Forum posts',
+ label: 'Posting Forum',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiReplyOutline' in icon ? icon['mdiReplyOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_FORUM_POSTS'
+ icon:
+ 'mdiReplyOutline' in icon
+ ? icon['mdiReplyOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_FORUM_POSTS',
},
{
href: '/job_companies/job_companies-list',
- label: 'Job companies',
+ label: 'Perusahaan Lowongan',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiOfficeBuildingOutline' in icon ? icon['mdiOfficeBuildingOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_JOB_COMPANIES'
+ icon:
+ 'mdiOfficeBuildingOutline' in icon
+ ? icon['mdiOfficeBuildingOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_JOB_COMPANIES',
},
{
href: '/job_listings/job_listings-list',
- label: 'Job listings',
+ label: 'Daftar Lowongan',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiBriefcaseOutline' in icon ? icon['mdiBriefcaseOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_JOB_LISTINGS'
+ icon:
+ 'mdiBriefcaseOutline' in icon
+ ? icon['mdiBriefcaseOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_JOB_LISTINGS',
},
{
href: '/job_applications/job_applications-list',
- label: 'Job applications',
+ label: 'Lamaran Kerja',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiFileAccountOutline' in icon ? icon['mdiFileAccountOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_JOB_APPLICATIONS'
+ icon:
+ 'mdiFileAccountOutline' in icon
+ ? icon['mdiFileAccountOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_JOB_APPLICATIONS',
},
{
href: '/user_credentials/user_credentials-list',
- label: 'User credentials',
+ label: 'Kredensial Pengguna',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiCertificateOutline' in icon ? icon['mdiCertificateOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_USER_CREDENTIALS'
+ icon:
+ 'mdiCertificateOutline' in icon
+ ? icon['mdiCertificateOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_USER_CREDENTIALS',
},
{
href: '/affiliate_programs/affiliate_programs-list',
- label: 'Affiliate programs',
+ label: 'Program Afiliasi',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiAccountNetworkOutline' in icon ? icon['mdiAccountNetworkOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_AFFILIATE_PROGRAMS'
+ icon:
+ 'mdiAccountNetworkOutline' in icon
+ ? icon['mdiAccountNetworkOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_AFFILIATE_PROGRAMS',
},
{
href: '/affiliate_memberships/affiliate_memberships-list',
- label: 'Affiliate memberships',
+ label: 'Keanggotaan Afiliasi',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiLinkBoxOutline' in icon ? icon['mdiLinkBoxOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_AFFILIATE_MEMBERSHIPS'
+ icon:
+ 'mdiLinkBoxOutline' in icon
+ ? icon['mdiLinkBoxOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_AFFILIATE_MEMBERSHIPS',
},
{
href: '/commissions/commissions-list',
- label: 'Commissions',
+ label: 'Komisi',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiCashCheck' in icon ? icon['mdiCashCheck' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_COMMISSIONS'
+ icon:
+ 'mdiCashCheck' in icon
+ ? icon['mdiCashCheck' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_COMMISSIONS',
},
{
href: '/geo_rules/geo_rules-list',
- label: 'Geo rules',
+ label: 'Aturan Geo',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiMapMarkerRadiusOutline' in icon ? icon['mdiMapMarkerRadiusOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_GEO_RULES'
+ icon:
+ 'mdiMapMarkerRadiusOutline' in icon
+ ? icon['mdiMapMarkerRadiusOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_GEO_RULES',
},
{
href: '/facta_queries/facta_queries-list',
- label: 'Facta queries',
+ label: 'Kueri Facta',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiMagnify' in icon ? icon['mdiMagnify' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_FACTA_QUERIES'
+ icon:
+ 'mdiMagnify' in icon
+ ? icon['mdiMagnify' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_FACTA_QUERIES',
},
{
href: '/facta_answers/facta_answers-list',
- label: 'Facta answers',
+ label: 'Jawaban Facta',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiShieldSearchOutline' in icon ? icon['mdiShieldSearchOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_FACTA_ANSWERS'
+ icon:
+ 'mdiShieldSearchOutline' in icon
+ ? icon['mdiShieldSearchOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_FACTA_ANSWERS',
},
{
href: '/subscriptions/subscriptions-list',
- label: 'Subscriptions',
+ label: 'Langganan',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiCrownOutline' in icon ? icon['mdiCrownOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_SUBSCRIPTIONS'
+ icon:
+ 'mdiCrownOutline' in icon
+ ? icon['mdiCrownOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_SUBSCRIPTIONS',
},
{
href: '/api_clients/api_clients-list',
- label: 'Api clients',
+ label: 'Klien API',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiApi' in icon ? icon['mdiApi' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_API_CLIENTS'
+ icon:
+ 'mdiApi' in icon
+ ? icon['mdiApi' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_API_CLIENTS',
},
{
href: '/audit_events/audit_events-list',
- label: 'Audit events',
+ label: 'Peristiwa Audit',
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
- icon: 'mdiClipboardTextOutline' in icon ? icon['mdiClipboardTextOutline' as keyof typeof icon] : icon.mdiTable ?? icon.mdiTable,
- permissions: 'READ_AUDIT_EVENTS'
+ icon:
+ 'mdiClipboardTextOutline' in icon
+ ? icon['mdiClipboardTextOutline' as keyof typeof icon]
+ : (icon.mdiTable ?? icon.mdiTable),
+ permissions: 'READ_AUDIT_EVENTS',
},
{
href: '/profile',
- label: 'Profile',
+ label: 'Profil',
icon: icon.mdiAccountCircle,
},
-
{
href: '/api-docs',
target: '_blank',
- label: 'Swagger API',
+ label: 'Dokumentasi API',
icon: icon.mdiFileCode,
- permissions: 'READ_API_DOCS'
+ permissions: 'READ_API_DOCS',
},
-]
+];
-export default menuAside
+export default menuAside;
diff --git a/frontend/src/menuNavBar.ts b/frontend/src/menuNavBar.ts
index a5dd956..ca1b7b1 100644
--- a/frontend/src/menuNavBar.ts
+++ b/frontend/src/menuNavBar.ts
@@ -10,8 +10,8 @@ import {
mdiThemeLightDark,
mdiGithub,
mdiVuejs,
-} from '@mdi/js'
-import { MenuNavBarItem } from './interfaces'
+} from '@mdi/js';
+import { MenuNavBarItem } from './interfaces';
const menuNavBar: MenuNavBarItem[] = [
{
@@ -19,7 +19,7 @@ const menuNavBar: MenuNavBarItem[] = [
menu: [
{
icon: mdiAccount,
- label: 'My Profile',
+ label: 'Profil Saya',
href: '/profile',
},
{
@@ -27,27 +27,25 @@ const menuNavBar: MenuNavBarItem[] = [
},
{
icon: mdiLogout,
- label: 'Log Out',
+ label: 'Keluar',
isLogout: true,
},
],
},
{
icon: mdiThemeLightDark,
- label: 'Light/Dark',
+ label: 'Terang/Gelap',
isDesktopNoLabel: true,
isToggleLightDark: true,
},
{
icon: mdiLogout,
- label: 'Log out',
+ label: 'Keluar',
isDesktopNoLabel: true,
isLogout: true,
},
-]
-
-export const webPagesNavBar = [
-
];
-export default menuNavBar
+export const webPagesNavBar = [];
+
+export default menuNavBar;
diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx
index 6f3ba90..97ade84 100644
--- a/frontend/src/pages/_app.tsx
+++ b/frontend/src/pages/_app.tsx
@@ -121,7 +121,7 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
setSteps(loginSteps);
setStepName('loginSteps');
setStepsEnabled(true);
- }else if (router.pathname === '/dashboard' && !isCompleted('appSteps')) {
+ }else if (router.pathname === '/vorta-commerce' && !isCompleted('appSteps')) {
setTimeout(() => {
setSteps(appSteps);
setStepName('appSteps');
@@ -149,8 +149,8 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
setStepsEnabled(false);
};
- const title = 'Vorta Universe MVP'
- const description = "Unified super-app for verified identity, chat-to-commerce, and citation-backed search with a single wallet."
+ const title = 'VORTA-COMMERCE'
+ const description = "Commerce operating system for products, inventory, orders, payments, shipping, marketing, reports, and settings."
const url = "https://flatlogic.com/"
const image = "https://project-screens.s3.amazonaws.com/screenshots/40285/app-hero-20260618-210659.png"
const imageWidth = '1920'
diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx
index d7e733c..db3922e 100644
--- a/frontend/src/pages/dashboard.tsx
+++ b/frontend/src/pages/dashboard.tsx
@@ -1,1340 +1,3 @@
-import * as icon from '@mdi/js';
-import Head from 'next/head'
-import React from 'react'
-import axios from 'axios';
-import type { ReactElement } from 'react'
-import LayoutAuthenticated from '../layouts/Authenticated'
-import SectionMain from '../components/SectionMain'
-import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'
-import BaseIcon from "../components/BaseIcon";
-import { getPageTitle } from '../config'
-import Link from "next/link";
+import VortaCommercePage from './vorta-commerce';
-import { hasPermission } from "../helpers/userPermissions";
-import { fetchWidgets } from '../stores/roles/rolesSlice';
-import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
-import { SmartWidget } from '../components/SmartWidget/SmartWidget';
-
-import { useAppDispatch, useAppSelector } from '../stores/hooks';
-const Dashboard = () => {
- const dispatch = useAppDispatch();
- const iconsColor = useAppSelector((state) => state.style.iconsColor);
- const corners = useAppSelector((state) => state.style.corners);
- const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
-
- const loadingMessage = 'Loading...';
-
-
- const [users, setUsers] = React.useState(loadingMessage);
- const [roles, setRoles] = React.useState(loadingMessage);
- const [permissions, setPermissions] = React.useState(loadingMessage);
- const [organizations, setOrganizations] = React.useState(loadingMessage);
- const [trust_profiles, setTrust_profiles] = React.useState(loadingMessage);
- const [identity_verifications, setIdentity_verifications] = React.useState(loadingMessage);
- const [wallets, setWallets] = React.useState(loadingMessage);
- const [wallet_transactions, setWallet_transactions] = React.useState(loadingMessage);
- const [chats, setChats] = React.useState(loadingMessage);
- const [chat_participants, setChat_participants] = React.useState(loadingMessage);
- const [messages, setMessages] = React.useState(loadingMessage);
- const [stores, setStores] = React.useState(loadingMessage);
- const [addresses, setAddresses] = React.useState(loadingMessage);
- const [product_categories, setProduct_categories] = React.useState(loadingMessage);
- const [products, setProducts] = React.useState(loadingMessage);
- const [orders, setOrders] = React.useState(loadingMessage);
- const [order_items, setOrder_items] = React.useState(loadingMessage);
- const [payments, setPayments] = React.useState(loadingMessage);
- const [product_reviews, setProduct_reviews] = React.useState(loadingMessage);
- const [shipments, setShipments] = React.useState(loadingMessage);
- const [posts, setPosts] = React.useState(loadingMessage);
- const [deep_dive_links, setDeep_dive_links] = React.useState(loadingMessage);
- const [citations, setCitations] = React.useState(loadingMessage);
- const [post_engagements, setPost_engagements] = React.useState(loadingMessage);
- const [rewards, setRewards] = React.useState(loadingMessage);
- const [forum_spaces, setForum_spaces] = React.useState(loadingMessage);
- const [forum_threads, setForum_threads] = React.useState(loadingMessage);
- const [forum_posts, setForum_posts] = React.useState(loadingMessage);
- const [job_companies, setJob_companies] = React.useState(loadingMessage);
- const [job_listings, setJob_listings] = React.useState(loadingMessage);
- const [job_applications, setJob_applications] = React.useState(loadingMessage);
- const [user_credentials, setUser_credentials] = React.useState(loadingMessage);
- const [affiliate_programs, setAffiliate_programs] = React.useState(loadingMessage);
- const [affiliate_memberships, setAffiliate_memberships] = React.useState(loadingMessage);
- const [commissions, setCommissions] = React.useState(loadingMessage);
- const [geo_rules, setGeo_rules] = React.useState(loadingMessage);
- const [facta_queries, setFacta_queries] = React.useState(loadingMessage);
- const [facta_answers, setFacta_answers] = React.useState(loadingMessage);
- const [subscriptions, setSubscriptions] = React.useState(loadingMessage);
- const [api_clients, setApi_clients] = React.useState(loadingMessage);
- const [audit_events, setAudit_events] = React.useState(loadingMessage);
-
-
- const [widgetsRole, setWidgetsRole] = React.useState({
- role: { value: '', label: '' },
- });
- const { currentUser } = useAppSelector((state) => state.auth);
- const { isFetchingQuery } = useAppSelector((state) => state.openAi);
-
- const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
-
-
- const organizationId = currentUser?.organizations?.id;
-
- async function loadData() {
- const entities = ['users','roles','permissions','organizations','trust_profiles','identity_verifications','wallets','wallet_transactions','chats','chat_participants','messages','stores','addresses','product_categories','products','orders','order_items','payments','product_reviews','shipments','posts','deep_dive_links','citations','post_engagements','rewards','forum_spaces','forum_threads','forum_posts','job_companies','job_listings','job_applications','user_credentials','affiliate_programs','affiliate_memberships','commissions','geo_rules','facta_queries','facta_answers','subscriptions','api_clients','audit_events',];
- const fns = [setUsers,setRoles,setPermissions,setOrganizations,setTrust_profiles,setIdentity_verifications,setWallets,setWallet_transactions,setChats,setChat_participants,setMessages,setStores,setAddresses,setProduct_categories,setProducts,setOrders,setOrder_items,setPayments,setProduct_reviews,setShipments,setPosts,setDeep_dive_links,setCitations,setPost_engagements,setRewards,setForum_spaces,setForum_threads,setForum_posts,setJob_companies,setJob_listings,setJob_applications,setUser_credentials,setAffiliate_programs,setAffiliate_memberships,setCommissions,setGeo_rules,setFacta_queries,setFacta_answers,setSubscriptions,setApi_clients,setAudit_events,];
-
- const requests = entities.map((entity, index) => {
-
- if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
- return axios.get(`/${entity.toLowerCase()}/count`);
- } else {
- fns[index](null);
- return Promise.resolve({data: {count: null}});
- }
-
- });
-
- Promise.allSettled(requests).then((results) => {
- results.forEach((result, i) => {
- if (result.status === 'fulfilled') {
- fns[i](result.value.data.count);
- } else {
- fns[i](result.reason.message);
- }
- });
- });
- }
-
- async function getWidgets(roleId) {
- await dispatch(fetchWidgets(roleId));
- }
- React.useEffect(() => {
- if (!currentUser) return;
- loadData().then();
- setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
- }, [currentUser]);
-
- React.useEffect(() => {
- if (!currentUser || !widgetsRole?.role?.value) return;
- getWidgets(widgetsRole?.role?.value || '').then();
- }, [widgetsRole?.role?.value]);
-
- return (
- <>
-
-
- {getPageTitle('Overview')}
-
-
-
-
- {''}
-
-
- {hasPermission(currentUser, 'CREATE_ROLES') && }
- {!!rolesWidgets.length &&
- hasPermission(currentUser, 'CREATE_ROLES') && (
-
- {`${widgetsRole?.role?.label || 'Users'}'s widgets`}
-
- )}
-
-
- {(isFetchingQuery || loading) && (
-
- {' '}
- Loading widgets...
-
- )}
-
- { rolesWidgets &&
- rolesWidgets.map((widget) => (
-
- ))}
-
-
- {!!rolesWidgets.length && }
-
-
-
-
- {hasPermission(currentUser, 'READ_USERS') &&
-
-
-
-
- Users
-
-
- {users}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_ROLES') &&
-
-
-
-
- Roles
-
-
- {roles}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PERMISSIONS') &&
-
-
-
-
- Permissions
-
-
- {permissions}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_ORGANIZATIONS') &&
-
-
-
-
- Organizations
-
-
- {organizations}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_TRUST_PROFILES') &&
-
-
-
-
- Trust profiles
-
-
- {trust_profiles}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_IDENTITY_VERIFICATIONS') &&
-
-
-
-
- Identity verifications
-
-
- {identity_verifications}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_WALLETS') &&
-
-
-
-
- Wallets
-
-
- {wallets}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_WALLET_TRANSACTIONS') &&
-
-
-
-
- Wallet transactions
-
-
- {wallet_transactions}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_CHATS') &&
-
-
-
-
- Chats
-
-
- {chats}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_CHAT_PARTICIPANTS') &&
-
-
-
-
- Chat participants
-
-
- {chat_participants}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_MESSAGES') &&
-
-
-
-
- Messages
-
-
- {messages}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_STORES') &&
-
-
-
-
- Stores
-
-
- {stores}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_ADDRESSES') &&
-
-
-
-
- Addresses
-
-
- {addresses}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PRODUCT_CATEGORIES') &&
-
-
-
-
- Product categories
-
-
- {product_categories}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PRODUCTS') &&
-
-
-
-
- Products
-
-
- {products}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_ORDERS') &&
-
-
-
-
- Orders
-
-
- {orders}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_ORDER_ITEMS') &&
-
-
-
-
- Order items
-
-
- {order_items}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PAYMENTS') &&
-
-
-
-
- Payments
-
-
- {payments}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PRODUCT_REVIEWS') &&
-
-
-
-
- Product reviews
-
-
- {product_reviews}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_SHIPMENTS') &&
-
-
-
-
- Shipments
-
-
- {shipments}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_POSTS') &&
-
-
-
-
- Posts
-
-
- {posts}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_DEEP_DIVE_LINKS') &&
-
-
-
-
- Deep dive links
-
-
- {deep_dive_links}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_CITATIONS') &&
-
-
-
-
- Citations
-
-
- {citations}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_POST_ENGAGEMENTS') &&
-
-
-
-
- Post engagements
-
-
- {post_engagements}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_REWARDS') &&
-
-
-
-
- Rewards
-
-
- {rewards}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_FORUM_SPACES') &&
-
-
-
-
- Forum spaces
-
-
- {forum_spaces}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_FORUM_THREADS') &&
-
-
-
-
- Forum threads
-
-
- {forum_threads}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_FORUM_POSTS') &&
-
-
-
-
- Forum posts
-
-
- {forum_posts}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_JOB_COMPANIES') &&
-
-
-
-
- Job companies
-
-
- {job_companies}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_JOB_LISTINGS') &&
-
-
-
-
- Job listings
-
-
- {job_listings}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_JOB_APPLICATIONS') &&
-
-
-
-
- Job applications
-
-
- {job_applications}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_USER_CREDENTIALS') &&
-
-
-
-
- User credentials
-
-
- {user_credentials}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_AFFILIATE_PROGRAMS') &&
-
-
-
-
- Affiliate programs
-
-
- {affiliate_programs}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_AFFILIATE_MEMBERSHIPS') &&
-
-
-
-
- Affiliate memberships
-
-
- {affiliate_memberships}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_COMMISSIONS') &&
-
-
-
-
- Commissions
-
-
- {commissions}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_GEO_RULES') &&
-
-
-
-
- Geo rules
-
-
- {geo_rules}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_FACTA_QUERIES') &&
-
-
-
-
- Facta queries
-
-
- {facta_queries}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_FACTA_ANSWERS') &&
-
-
-
-
- Facta answers
-
-
- {facta_answers}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_SUBSCRIPTIONS') &&
-
-
-
-
- Subscriptions
-
-
- {subscriptions}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_API_CLIENTS') &&
-
-
-
-
- Api clients
-
-
- {api_clients}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_AUDIT_EVENTS') &&
-
-
-
-
- Audit events
-
-
- {audit_events}
-
-
-
-
-
-
-
- }
-
-
-
-
- >
- )
-}
-
-Dashboard.getLayout = function getLayout(page: ReactElement) {
- return
{page}
-}
-
-export default Dashboard
+export default VortaCommercePage;
diff --git a/frontend/src/pages/error.tsx b/frontend/src/pages/error.tsx
index 4a6116e..51e09fe 100644
--- a/frontend/src/pages/error.tsx
+++ b/frontend/src/pages/error.tsx
@@ -17,7 +17,7 @@ export default function Error() {
}
+ footer={ }
>
Unhandled exception
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index 1912a06..ff1ea24 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -1,166 +1,830 @@
-
-import React, { useEffect, useState } from 'react';
-import type { ReactElement } from 'react';
+import React, {
+ FormEvent,
+ ReactElement,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox';
-import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
-import BaseDivider from '../components/BaseDivider';
-import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
-import { useAppSelector } from '../stores/hooks';
-import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
-import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
+type TrustForm = {
+ profileName: string;
+ profileType: 'creator' | 'umkm' | 'company';
+ verifiedDocuments: string;
+ hasBiometric: boolean;
+ hasTaxId: boolean;
+ hasTransactionHistory: boolean;
+ citedSources: string;
+ verifiedReviews: boolean;
+ localPresence: boolean;
+};
-export default function Starter() {
- const [illustrationImage, setIllustrationImage] = useState({
- src: undefined,
- photographer: undefined,
- photographer_url: undefined,
- })
- const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
- const [contentType, setContentType] = useState('image');
- const [contentPosition, setContentPosition] = useState('background');
- const textColor = useAppSelector((state) => state.style.linkColor);
+type TrustReport = TrustForm & {
+ id: string;
+ score: number;
+ tier: string;
+ createdAt: string;
+ recommendations: string[];
+};
- const title = 'Vorta Universe MVP'
+const defaultForm: TrustForm = {
+ profileName: '',
+ profileType: 'umkm',
+ verifiedDocuments: '2',
+ hasBiometric: true,
+ hasTaxId: false,
+ hasTransactionHistory: true,
+ citedSources: '1',
+ verifiedReviews: false,
+ localPresence: true,
+};
- // Fetch Pexels image/video
- useEffect(() => {
- async function fetchData() {
- const image = await getPexelsImage();
- const video = await getPexelsVideo();
- setIllustrationImage(image);
- setIllustrationVideo(video);
- }
- fetchData();
- }, []);
+const storageKey = 'vorta-trust-reports';
- const imageBlock = (image) => (
-
+const profileTypeLabels: Record
= {
+ creator: 'Kreator',
+ umkm: 'UMKM',
+ company: 'Perusahaan',
+};
+
+const getTier = (score: number) => {
+ if (score >= 86) return 'Nexus Prime';
+ if (score >= 70) return 'Pertumbuhan Terverifikasi';
+ if (score >= 31) return 'Layak Pulse';
+
+ return 'Perlu Verifikasi';
+};
+
+const clampNumber = (value: string, min: number, max: number) => {
+ const parsed = Number.parseInt(value, 10);
+
+ if (Number.isNaN(parsed)) return min;
+
+ return Math.min(Math.max(parsed, min), max);
+};
+
+const buildRecommendations = (form: TrustForm, score: number) => {
+ const recommendations = [];
+ const documents = clampNumber(form.verifiedDocuments, 0, 4);
+ const sources = clampNumber(form.citedSources, 0, 5);
+
+ if (!form.hasBiometric)
+ recommendations.push(
+ 'Aktifkan pencocokan wajah untuk mengikat akun dengan manusia asli.',
+ );
+ if (!form.hasTaxId)
+ recommendations.push(
+ 'Tambahkan NPWP atau legalitas bisnis untuk transaksi bernilai tinggi.',
+ );
+ if (documents < 3)
+ recommendations.push(
+ 'Unggah minimal tiga dokumen valid agar skor Nexus naik lebih cepat.',
+ );
+ if (sources < 3)
+ recommendations.push(
+ 'Tambahkan Deep Dive Link primer agar konten lolos cek sitasi Facta.',
+ );
+ if (!form.verifiedReviews)
+ recommendations.push(
+ 'Kunci ulasan hanya dari pembeli terverifikasi untuk ulasan bebas manipulasi.',
+ );
+ if (score < 31)
+ recommendations.push(
+ 'Prioritaskan verifikasi dasar agar dapat menulis di Vorta Pulse.',
);
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
- Your browser does not support the video tag.
-
-
-
)
- }
+ return recommendations.length
+ ? recommendations
+ : ['Profil siap diprioritaskan untuk visibilitas organik lintas VORTA.'];
+};
+
+const calculateReport = (form: TrustForm): TrustReport => {
+ const documents = clampNumber(form.verifiedDocuments, 0, 4);
+ const sources = clampNumber(form.citedSources, 0, 5);
+ const score = Math.min(
+ 100,
+ 14 +
+ documents * 9 +
+ (form.hasBiometric ? 18 : 0) +
+ (form.hasTaxId ? 12 : 0) +
+ (form.hasTransactionHistory ? 14 : 0) +
+ sources * 4 +
+ (form.verifiedReviews ? 10 : 0) +
+ (form.localPresence ? 6 : 0),
+ );
+
+ return {
+ ...form,
+ verifiedDocuments: String(documents),
+ citedSources: String(sources),
+ id: `${Date.now()}`,
+ score,
+ tier: getTier(score),
+ createdAt: new Date().toISOString(),
+ recommendations: buildRecommendations(form, score),
+ };
+};
+
+const scoreGradient = (score: number) => {
+ if (score >= 70) return 'from-emerald-400 to-cyan-300';
+ if (score >= 31) return 'from-amber-300 to-orange-400';
+
+ return 'from-rose-400 to-red-500';
+};
+
+const scrollToSection = (sectionId: string) => {
+ const element = document.getElementById(sectionId);
+
+ if (element) {
+ element.scrollIntoView({ behavior: 'smooth', block: 'start' });
+ }
+};
+
+export default function VortaLanding() {
+ const [form, setForm] = useState(defaultForm);
+ const [reports, setReports] = useState([]);
+ const [activeReportId, setActiveReportId] = useState('');
+ const [errorMessage, setErrorMessage] = useState('');
+
+ useEffect(() => {
+ try {
+ const stored = window.localStorage.getItem(storageKey);
+ const parsedReports = stored ? (JSON.parse(stored) as TrustReport[]) : [];
+
+ if (Array.isArray(parsedReports)) {
+ const normalizedReports = parsedReports.map((report) => ({
+ ...report,
+ tier: getTier(report.score),
+ recommendations: buildRecommendations(report, report.score),
+ }));
+
+ setReports(normalizedReports);
+ setActiveReportId(normalizedReports[0]?.id || '');
+ }
+ } catch (error) {
+ console.error(
+ 'Failed to read VORTA trust reports from localStorage:',
+ error,
+ );
+ window.localStorage.removeItem(storageKey);
+ }
+ }, []);
+
+ useEffect(() => {
+ window.localStorage.setItem(storageKey, JSON.stringify(reports));
+ }, [reports]);
+
+ const activeReport = useMemo(
+ () => reports.find((report) => report.id === activeReportId) || reports[0],
+ [activeReportId, reports],
+ );
+
+ const updateField = (field: keyof TrustForm, value: string | boolean) => {
+ setForm((current) => ({ ...current, [field]: value }));
+ };
+
+ const saveReport = (nextForm: TrustForm) => {
+ const report = calculateReport(nextForm);
+
+ setReports((current) => [report, ...current].slice(0, 6));
+ setActiveReportId(report.id);
+ window.setTimeout(() => scrollToSection('reports'), 100);
+ };
+
+ const handleSubmit = (event: FormEvent) => {
+ event.preventDefault();
+ const trimmedName = form.profileName.trim();
+
+ if (trimmedName.length < 3) {
+ setErrorMessage(
+ 'Nama profil minimal 3 karakter agar laporan dapat dibuat.',
+ );
+ return;
+ }
+
+ setErrorMessage('');
+ saveReport({ ...form, profileName: trimmedName });
+ };
+
+ const handleSampleReport = () => {
+ const sampleForm: TrustForm = {
+ profileName: 'Warung Nusantara Bandung',
+ profileType: 'umkm',
+ verifiedDocuments: '4',
+ hasBiometric: true,
+ hasTaxId: true,
+ hasTransactionHistory: true,
+ citedSources: '4',
+ verifiedReviews: true,
+ localPresence: true,
};
+ setForm(sampleForm);
+ setErrorMessage('');
+ saveReport(sampleForm);
+ };
+
+ const handleResetForm = () => {
+ setForm(defaultForm);
+ setErrorMessage('');
+ scrollToSection('trust-flow');
+ };
+
+ const stats = [
+ { label: 'kanal utama', value: '4 mode' },
+ { label: 'ruang kerja', value: '1 app' },
+ { label: 'akses ringan', value: '<3 detik' },
+ ];
+
+ const pillars = [
+ [
+ 'Mega Super-App',
+ 'Chat bisnis, sosial, marketplace, dan dompet dalam satu ruang kerja ringan.',
+ ],
+ [
+ 'Vorta Nexus',
+ 'Skor kepercayaan 0-100 untuk identitas, bisnis, ulasan, dan akses komunitas.',
+ ],
+ [
+ 'Facta.AI',
+ 'Riset bebas SEO spam dengan Truth Score dan peta rujukan langsung.',
+ ],
+ [
+ 'Vorta Synapse',
+ 'Protokol distribusi, afiliasi, dan logistik pintar untuk ekonomi kreator.',
+ ],
+ ];
+
return (
-
+ <>
-
{getPageTitle('Starter Page')}
+
{getPageTitle('01 Mega Super-App')}
+
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
-
This is a React.js/Node.js app generated by the Flatlogic Web App Generator
-
For guides and documentation please check
- your local README.md and the Flatlogic documentation
+
+
+
+
+
+
+
+
+ V
+
+ VORTA UNIVERSE
+
+
+ {[
+ ['pillars', 'Super-App'],
+ ['trust-flow', 'Skor'],
+ ['reports', 'Laporan'],
+ ].map(([sectionId, label]) => (
+ scrollToSection(sectionId)}
+ className='rounded-full px-4 py-2 transition hover:bg-white/10 hover:text-white focus:outline-none focus:ring-2 focus:ring-cyan-300/50'
+ >
+ {label}
+
+ ))}
-
-
+
+
+
+
+ {[
+ ['pillars', 'Super-App'],
+ ['trust-flow', 'Skor'],
+ ['reports', 'Laporan'],
+ ].map(([sectionId, label]) => (
+ scrollToSection(sectionId)}
+ className='rounded-full border border-white/10 bg-white/[0.08] px-4 py-2 transition hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-cyan-300/50'
+ >
+ {label}
+
+ ))}
+
+
+
+
+
+ Fokus pertama: 01 Mega Super-App
+
+
+ Chat bisnis, sosial, marketplace, dan dompet dalam satu ruang
+ kerja ringan.
+
+
+ Mulai dari percakapan tim, feed komunitas, etalase produk,
+ pembayaran, sampai riwayat transaksi—semuanya berjalan di satu
+ workspace cepat dengan identitas dan skor kepercayaan bawaan.
+
+
scrollToSection('pillars')}
+ label='Lihat Alur Super-App'
+ color='success'
+ roundedFull
+ className='border-emerald-300 bg-[#1DE9B6] px-7 py-3 text-slate-950 hover:bg-white'
/>
+ scrollToSection('trust-flow')}
+ label='Simulasikan Kepercayaan'
+ color='white'
+ outline
+ roundedFull
+ className='border-white/30 bg-white/5 px-7 py-3 text-white hover:bg-white hover:text-slate-950'
+ />
+
+
+
+ {stats.map((stat) => (
+
+
+ {stat.value}
+
+
+ {stat.label}
+
+
+ ))}
+
+
-
-
-
-
-
-
-
© 2026 {title} . All rights reserved
-
- Privacy Policy
-
-
+
+
+
+
+
+
+
+ Pratinjau ruang kerja langsung
+
+
+ Mega Super-App Workspace
+
+
+
+ AKTIF
+
+
+
+
+
+ {[
+ 'Chat bisnis',
+ 'Feed sosial',
+ 'Marketplace',
+ 'Dompet digital',
+ ].map((item) => (
+
+ ))}
+
+
+
+
+
+ Aktivitas terpadu hari ini
+
+
128
+
+
+ Transaksi & pesan sinkron
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+ Fokus pertama: 01 Mega Super-App
+
+
+ Satu ruang kerja ringan yang menghubungkan percakapan,
+ komunitas, perdagangan, pembayaran, dan reputasi pengguna.
+
+
+
+ {pillars.map(([title, description], index) => (
+
+
+ 0{index + 1}
+
+ {title}
+
+ {description}
+
+
+ ))}
+
+
+
+
+
+
+
+
+ Mesin kepercayaan bawaan
+
+
+ Skor Kepercayaan untuk Super-App
+
+
+ Setiap akun, toko, percakapan, ulasan, dan transaksi bisa
+ diberi sinyal kepercayaan agar marketplace dan komunitas tetap
+ aman tanpa terasa berat bagi pengguna.
+
+
+ Aturan validasi: nama
+ profil wajib minimal 3 karakter. Skor 31+ menandakan profil
+ layak masuk feed sosial, chat bisnis, dan marketplace publik.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Daftar Laporan Kepercayaan
+
+
+ Pilih laporan untuk membuka detail simulasi.
+
+
+
+ {reports.length ? (
+ reports.map((report) => (
+
setActiveReportId(report.id)}
+ className={`w-full rounded-3xl border p-4 text-left transition focus:outline-none focus:ring-4 focus:ring-cyan-300/30 ${activeReport?.id === report.id ? 'border-cyan-300 bg-cyan-300/15' : 'border-white/10 bg-slate-950/30 hover:border-white/30 hover:bg-white/10'}`}
+ >
+
+
+
{report.profileName}
+
+ {profileTypeLabels[report.profileType]} ·{' '}
+ {new Date(report.createdAt).toLocaleDateString(
+ 'id-ID',
+ )}
+
+
+
+ {report.score}
+
+
+
+ ))
+ ) : (
+
+
+ Belum ada laporan. Isi simulator di atas untuk membuat
+ Laporan Kepercayaan pertama.
+
+
+ Buat Contoh Laporan
+
+
+ )}
+
+
+
+
+ {activeReport ? (
+
+
+
+ Detail Laporan Kepercayaan
+
+
+
+
+ {activeReport.profileName}
+
+
+ {getTier(activeReport.score)}
+
+
+
+ {activeReport.score}
+
+
+
+
+
+
+
+
Ringkasan sinyal
+
+
+
Dokumen
+
+ {activeReport.verifiedDocuments}/4
+
+
+
+
Deep Dive Links
+
+ {activeReport.citedSources}/5
+
+
+
+
Akses Pulse
+
+ {activeReport.score >= 31
+ ? 'Memenuhi syarat'
+ : 'Terkunci'}
+
+
+
+
Integritas ulasan
+
+ {activeReport.verifiedReviews
+ ? 'Terlindungi'
+ : 'Terbuka'}
+
+
+
+
+
+
Aksi terbaik berikutnya
+
+ {activeReport.recommendations.map((recommendation) => (
+
+
+ {recommendation}
+
+ ))}
+
+
+
+
+ ) : (
+
+
+ 🛡️
+
+
+ Detail laporan akan muncul di sini
+
+
+ Buat Laporan Kepercayaan pertama untuk melihat tier,
+ progress bar, dan rekomendasi.
+
+
+ )}
+
+
+
+
+
+
+ >
);
}
-Starter.getLayout = function getLayout(page: ReactElement) {
+VortaLanding.getLayout = function getLayout(page: ReactElement) {
return {page} ;
};
-
diff --git a/frontend/src/pages/login.tsx b/frontend/src/pages/login.tsx
index 78fe269..bfae6e2 100644
--- a/frontend/src/pages/login.tsx
+++ b/frontend/src/pages/login.tsx
@@ -44,7 +44,7 @@ export default function Login() {
password: '9654d2ec',
remember: true })
- const title = 'Vorta Universe MVP'
+ const title = 'VORTA-COMMERCE'
// Fetch Pexels image/video
useEffect( () => {
@@ -65,7 +65,7 @@ export default function Login() {
// Redirect to dashboard if user is logged in
useEffect(() => {
if (currentUser?.id) {
- router.push('/dashboard');
+ router.push('/vorta-commerce');
}
}, [currentUser?.id, router]);
// Show error message if there is one
diff --git a/frontend/src/pages/mega-super-app.tsx b/frontend/src/pages/mega-super-app.tsx
new file mode 100644
index 0000000..077579e
--- /dev/null
+++ b/frontend/src/pages/mega-super-app.tsx
@@ -0,0 +1,660 @@
+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 BusinessMessage = {
+ id: number;
+ sender: string;
+ text: string;
+ time: string;
+ mine?: boolean;
+};
+
+type SocialPost = {
+ id: number;
+ author: string;
+ text: string;
+ tag: string;
+ likes: number;
+};
+
+type Product = {
+ id: number;
+ name: string;
+ seller: string;
+ price: number;
+ badge: string;
+ stock: number;
+};
+
+type CartItem = Product & {
+ qty: number;
+};
+
+type WalletTransaction = {
+ id: number;
+ label: string;
+ amount: number;
+ type: 'in' | 'out';
+ note: string;
+};
+
+type Activity = {
+ id: number;
+ module: string;
+ text: 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 MegaSuperAppPage = () => {
+ const { currentUser } = useAppSelector((state) => state.auth);
+ const iconsColor = useAppSelector((state) => state.style.iconsColor);
+
+ const [activeConversation, setActiveConversation] = React.useState('Mitra Grosir Bandung');
+ const [messageInput, setMessageInput] = React.useState('');
+ const [postInput, setPostInput] = React.useState('');
+ const [walletInput, setWalletInput] = React.useState('25000');
+ const [walletNote, setWalletNote] = React.useState('Top up saldo operasional');
+ const [balance, setBalance] = React.useState(1250000);
+
+ const [messages, setMessages] = React.useState([
+ {
+ id: 1,
+ sender: 'Mitra Grosir Bandung',
+ text: 'Stok paket reseller sudah siap. Mau langsung dibuatkan invoice marketplace?',
+ time: '09:12',
+ },
+ {
+ id: 2,
+ sender: 'Anda',
+ text: 'Ya, kirim 12 paket. Pembayaran saya pakai dompet workspace.',
+ time: '09:15',
+ mine: true,
+ },
+ {
+ id: 3,
+ sender: 'Mitra Grosir Bandung',
+ text: 'Siap. Saya tautkan order ke chat ini supaya tracking reputasi ikut naik.',
+ time: '09:17',
+ },
+ ]);
+
+ const [posts, setPosts] = React.useState([
+ {
+ id: 1,
+ author: 'Komunitas Seller Lokal',
+ text: 'Promo bareng akhir pekan: bundling produk komunitas, cashback dompet, dan diskusi live di feed.',
+ tag: 'Kolaborasi',
+ likes: 42,
+ },
+ {
+ id: 2,
+ author: 'Rina Kopi Nusantara',
+ text: 'Baru upload katalog kopi 250gr. Pembeli dari chat bisnis bisa checkout tanpa pindah aplikasi.',
+ tag: 'Marketplace',
+ likes: 27,
+ },
+ ]);
+
+ const products: Product[] = [
+ {
+ id: 1,
+ name: 'Paket Reseller Kopi 12 pcs',
+ seller: 'Kopi Nusantara',
+ price: 360000,
+ badge: 'Terhubung chat',
+ stock: 18,
+ },
+ {
+ id: 2,
+ name: 'Voucher Iklan Feed Komunitas',
+ seller: '01 Ads Lite',
+ price: 150000,
+ badge: 'Naikkan reputasi',
+ stock: 40,
+ },
+ {
+ id: 3,
+ name: 'Jasa Desain Katalog UMKM',
+ seller: 'Studio Pasar Sosial',
+ price: 275000,
+ badge: 'Escrow dompet',
+ stock: 7,
+ },
+ ];
+
+ const [cart, setCart] = React.useState([
+ { ...products[0], qty: 1 },
+ ]);
+
+ const [transactions, setTransactions] = React.useState([
+ {
+ id: 1,
+ label: 'Pembayaran order kopi',
+ amount: 360000,
+ type: 'out',
+ note: 'Marketplace • escrow aktif',
+ },
+ {
+ id: 2,
+ label: 'Cashback komunitas seller',
+ amount: 50000,
+ type: 'in',
+ note: 'Reward sosial',
+ },
+ {
+ id: 3,
+ label: 'Top up dompet',
+ amount: 500000,
+ type: 'in',
+ note: 'Bank transfer',
+ },
+ ]);
+
+ const [activities, setActivities] = React.useState([
+ {
+ id: 1,
+ module: 'Chat',
+ text: 'Negosiasi dengan Mitra Grosir Bandung ditautkan ke order #MSA-1201.',
+ time: 'Baru saja',
+ },
+ {
+ id: 2,
+ module: 'Marketplace',
+ text: '1 produk masuk keranjang dan siap dibayar dari Dompet 01.',
+ time: '3 menit lalu',
+ },
+ {
+ id: 3,
+ module: 'Sosial',
+ text: 'Posting komunitas seller mendapat 42 suka dan 8 calon pembeli.',
+ time: '12 menit lalu',
+ },
+ ]);
+
+ const userName = currentUser?.firstName || currentUser?.email || 'Pengguna 01';
+ const cartTotal = cart.reduce((sum, item) => sum + item.price * item.qty, 0);
+ const projectedBalance = balance - cartTotal;
+
+ const addActivity = (module: string, text: string) => {
+ setActivities((current) => [
+ {
+ id: Date.now(),
+ module,
+ text,
+ time: 'Baru saja',
+ },
+ ...current.slice(0, 5),
+ ]);
+ };
+
+ const sendMessage = () => {
+ const trimmed = messageInput.trim();
+
+ if (!trimmed) {
+ return;
+ }
+
+ setMessages((current) => [
+ ...current,
+ {
+ id: Date.now(),
+ sender: 'Anda',
+ text: trimmed,
+ time: 'Sekarang',
+ mine: true,
+ },
+ ]);
+ setMessageInput('');
+ addActivity('Chat', `Pesan bisnis baru dikirim ke ${activeConversation}.`);
+ };
+
+ const publishPost = () => {
+ const trimmed = postInput.trim();
+
+ if (!trimmed) {
+ return;
+ }
+
+ setPosts((current) => [
+ {
+ id: Date.now(),
+ author: String(userName),
+ text: trimmed,
+ tag: 'Update Bisnis',
+ likes: 0,
+ },
+ ...current,
+ ]);
+ setPostInput('');
+ addActivity('Sosial', 'Update baru diterbitkan ke feed komunitas.');
+ };
+
+ const addToCart = (product: Product) => {
+ setCart((current) => {
+ const existing = current.find((item) => item.id === product.id);
+
+ if (existing) {
+ return current.map((item) =>
+ item.id === product.id ? { ...item, qty: item.qty + 1 } : item,
+ );
+ }
+
+ return [...current, { ...product, qty: 1 }];
+ });
+ addActivity('Marketplace', `${product.name} ditambahkan ke keranjang terpadu.`);
+ };
+
+ const checkoutCart = () => {
+ if (!cart.length || cartTotal > balance) {
+ return;
+ }
+
+ setBalance((current) => current - cartTotal);
+ setTransactions((current) => [
+ {
+ id: Date.now(),
+ label: 'Checkout marketplace terpadu',
+ amount: cartTotal,
+ type: 'out',
+ note: `${cart.length} item • escrow dompet`,
+ },
+ ...current,
+ ]);
+ setCart([]);
+ addActivity('Dompet', `Checkout ${formatCurrency(cartTotal)} berhasil memakai Dompet 01.`);
+ };
+
+ const addWalletTransaction = () => {
+ const amount = Number(walletInput);
+ const trimmedNote = walletNote.trim() || 'Top up dompet workspace';
+
+ if (!amount || amount <= 0) {
+ return;
+ }
+
+ setBalance((current) => current + amount);
+ setTransactions((current) => [
+ {
+ id: Date.now(),
+ label: 'Top up cepat',
+ amount,
+ type: 'in',
+ note: trimmedNote,
+ },
+ ...current,
+ ]);
+ setWalletInput('');
+ setWalletNote('');
+ addActivity('Dompet', `Saldo bertambah ${formatCurrency(amount)} dari top up cepat.`);
+ };
+
+ const conversations = [
+ {
+ name: 'Mitra Grosir Bandung',
+ status: 'Invoice marketplace siap',
+ unread: 2,
+ },
+ {
+ name: 'Komunitas Seller Lokal',
+ status: 'Diskusi promo bareng',
+ unread: 4,
+ },
+ {
+ name: 'Customer Prioritas',
+ status: 'Butuh update pengiriman',
+ unread: 1,
+ },
+ ];
+
+ return (
+ <>
+
+ {getPageTitle('Mega Super-App')}
+
+
+
+
+
+
+
+
+
+
+
+ Workspace ringan untuk bisnis, sosial, marketplace, dan dompet
+
+
+ Halo {userName}, kelola semua aktivitas dari satu ruang kerja.
+
+
+ MVP ini menyatukan chat bisnis, feed komunitas, katalog marketplace,
+ keranjang, pembayaran dompet, dan aktivitas terpadu tanpa pindah halaman.
+
+
+ {[
+ { label: 'Chat aktif', value: conversations.length, iconName: 'mdiChatProcessingOutline' },
+ { label: 'Posting sosial', value: posts.length, iconName: 'mdiPostOutline' },
+ { label: 'Produk live', value: products.length, iconName: 'mdiStorefrontOutline' },
+ { label: 'Saldo dompet', value: formatCurrency(balance), iconName: 'mdiWalletOutline' },
+ ].map((item) => (
+
+
+
+ {item.label}
+
+
{item.value}
+
+ ))}
+
+
+
+
+
+
Status workspace
+
Siap operasional
+
+
+ Live MVP
+
+
+
+ {activities.slice(0, 4).map((activity) => (
+
+
+ {activity.module}
+ {activity.time}
+
+
{activity.text}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
Ruang chat aktif
+
{activeConversation}
+
+
+ {messages.map((message) => (
+
+
+
+ {message.sender} • {message.time}
+
+
{message.text}
+
+
+ ))}
+
+
+
+ setMessageInput(event.target.value)}
+ onKeyDown={(event) => {
+ if (event.key === 'Enter') {
+ sendMessage();
+ }
+ }}
+ className="h-11 flex-1 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-900"
+ placeholder="Tulis pesan, buat invoice, atau tautkan produk..."
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
Dompet 01
+
Saldo, top up, checkout, dan escrow.
+
+
+
+
+
Saldo tersedia
+
{formatCurrency(balance)}
+
Estimasi setelah checkout: {formatCurrency(projectedBalance)}
+
+
+ setWalletInput(event.target.value)}
+ type="number"
+ min="0"
+ className="h-10 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-900"
+ placeholder="Nominal top up"
+ />
+ setWalletNote(event.target.value)}
+ className="h-10 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-900"
+ placeholder="Catatan"
+ />
+
+
+
+ {transactions.slice(0, 4).map((transaction) => (
+
+
+
{transaction.label}
+
{transaction.note}
+
+
+ {transaction.type === 'in' ? '+' : '-'}{formatCurrency(transaction.amount)}
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
Feed Sosial Bisnis
+
Bangun komunitas dan reputasi sambil menjual.
+
+
+
+
+
+
+
+
+
+
+
Marketplace Ringan
+
Produk, chat seller, keranjang, dan pembayaran dompet.
+
+
+
+
+ {products.map((product) => (
+
+
+ {product.badge}
+
+
{product.name}
+
{product.seller} • Stok {product.stock}
+
{formatCurrency(product.price)}
+
addToCart(product)}
+ />
+
+ ))}
+
+
+
+
+
Keranjang Terpadu
+ {cart.length} item
+
+ {cart.length ? (
+
+ {cart.map((item) => (
+
+ {item.name} × {item.qty}
+ {formatCurrency(item.price * item.qty)}
+
+ ))}
+
+ ) : (
+
Keranjang kosong. Tambahkan produk untuk checkout.
+ )}
+
+ Total
+ {formatCurrency(cartTotal)}
+
+
balance ? 'Saldo Tidak Cukup' : 'Bayar dari Dompet'}
+ icon={resolveIcon('mdiWalletPlusOutline')}
+ color={cartTotal > balance ? 'danger' : 'info'}
+ disabled={!cart.length || cartTotal > balance}
+ className="mt-3 w-full"
+ onClick={checkoutCart}
+ />
+
+
+
+
+
+ >
+ );
+};
+
+MegaSuperAppPage.getLayout = function getLayout(page: ReactElement) {
+ return {page} ;
+};
+
+export default MegaSuperAppPage;
diff --git a/frontend/src/pages/privacy-policy.tsx b/frontend/src/pages/privacy-policy.tsx
index 90b054b..dbbe884 100644
--- a/frontend/src/pages/privacy-policy.tsx
+++ b/frontend/src/pages/privacy-policy.tsx
@@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
export default function PrivacyPolicy() {
- const title = 'Vorta Universe MVP'
+ const title = 'VORTA-COMMERCE'
const [projectUrl, setProjectUrl] = useState('');
useEffect(() => {
diff --git a/frontend/src/pages/search.tsx b/frontend/src/pages/search.tsx
index 00f5168..d51660a 100644
--- a/frontend/src/pages/search.tsx
+++ b/frontend/src/pages/search.tsx
@@ -1,9 +1,7 @@
import React, { ReactElement, useEffect, useState } from 'react';
import Head from 'next/head';
import 'react-datepicker/dist/react-datepicker.css';
-import { useAppDispatch } from '../stores/hooks';
-
-import { useAppSelector } from '../stores/hooks';
+import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router';
import LayoutAuthenticated from '../layouts/Authenticated';
@@ -76,7 +74,7 @@ const SearchView = () => {
router.push('/dashboard')}
+ onClick={() => router.push('/vorta-commerce')}
/>
diff --git a/frontend/src/pages/terms-of-use.tsx b/frontend/src/pages/terms-of-use.tsx
index 2c4ae84..ba53335 100644
--- a/frontend/src/pages/terms-of-use.tsx
+++ b/frontend/src/pages/terms-of-use.tsx
@@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest';
import { getPageTitle } from '../config';
export default function PrivacyPolicy() {
- const title = 'Vorta Universe MVP';
+ const title = 'VORTA-COMMERCE';
const [projectUrl, setProjectUrl] = useState('');
useEffect(() => {
diff --git a/frontend/src/pages/vorta-commerce.tsx b/frontend/src/pages/vorta-commerce.tsx
new file mode 100644
index 0000000..91c2d0f
--- /dev/null
+++ b/frontend/src/pages/vorta-commerce.tsx
@@ -0,0 +1,1362 @@
+import * as icon from '@mdi/js';
+import Head from 'next/head';
+import React, { ReactElement, useMemo, useState } from 'react';
+import BaseButton from '../components/BaseButton';
+import BaseIcon from '../components/BaseIcon';
+import CardBox from '../components/CardBox';
+import SectionMain from '../components/SectionMain';
+import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
+import LayoutAuthenticated from '../layouts/Authenticated';
+import { getPageTitle } from '../config';
+import { useAppSelector } from '../stores/hooks';
+
+type TabId =
+ | 'dashboard'
+ | 'products'
+ | 'inventory'
+ | 'orders'
+ | 'customers'
+ | 'payments'
+ | 'shipping'
+ | 'marketing'
+ | 'reports'
+ | 'settings';
+
+type Product = {
+ id: string;
+ name: string;
+ category: string;
+ price: number;
+ stock: number;
+ reorderPoint: number;
+ reorderPack: number;
+ description: string;
+ sold: number;
+ margin: number;
+ trend: number;
+};
+
+type Customer = {
+ id: string;
+ name: string;
+ phone: string;
+ email: string;
+ address: string;
+ segment: 'VIP' | 'Reguler' | 'Baru';
+ orders: number;
+ lifetimeValue: number;
+};
+
+type OrderStatus = 'Baru' | 'Diproses' | 'Dikirim' | 'Selesai';
+type PaymentStatus = 'Menunggu' | 'Berhasil' | 'Gagal' | 'Refund';
+type ShipmentStatus = 'Menunggu Pick Up' | 'Dikirim' | 'Terkirim' | 'Tahan QC';
+
+type Order = {
+ id: string;
+ customerId: string;
+ total: number;
+ status: OrderStatus;
+ paymentStatus: PaymentStatus;
+ shipmentStatus: ShipmentStatus;
+ channel: 'Web Store' | 'Marketplace' | 'Social Commerce' | 'B2B';
+ createdAt: string;
+ items: string[];
+};
+
+type Payment = {
+ id: string;
+ orderId: string;
+ method: 'VA Bank' | 'QRIS' | 'Kartu Kredit' | 'COD' | 'Wallet';
+ amount: number;
+ status: PaymentStatus;
+};
+
+type Shipment = {
+ id: string;
+ orderId: string;
+ destination: string;
+ courier: string;
+ eta: string;
+ status: ShipmentStatus;
+ risk: 'Rendah' | 'Sedang' | 'Tinggi';
+};
+
+type Campaign = {
+ id: string;
+ name: string;
+ channel: 'Email' | 'Ads' | 'Affiliate' | 'Push';
+ budget: number;
+ status: 'Draft' | 'Aktif' | 'Selesai';
+ conversion: number;
+ revenue: number;
+};
+
+type LedgerItem = {
+ id: string;
+ title: string;
+ detail: string;
+ time: string;
+ tone: 'info' | 'success' | 'warning' | 'danger';
+};
+
+type Settings = {
+ storeName: string;
+ currency: 'IDR';
+ fulfillmentMode: 'Otomatis' | 'Manual Review';
+ lowStockAlert: boolean;
+ paymentAutoCapture: boolean;
+};
+
+const initialProducts: Product[] = [
+ {
+ id: 'SKU-EL-001',
+ name: 'Vorta Smart Speaker X1',
+ category: 'Elektronik',
+ price: 1299000,
+ stock: 42,
+ reorderPoint: 12,
+ reorderPack: 30,
+ description: 'Speaker pintar untuk rumah, toko, dan kantor kecil.',
+ sold: 318,
+ margin: 28,
+ trend: 12,
+ },
+ {
+ id: 'SKU-FA-044',
+ name: 'Nusa Everyday Tote',
+ category: 'Fashion',
+ price: 249000,
+ stock: 9,
+ reorderPoint: 16,
+ reorderPack: 60,
+ description: 'Tas kanvas premium untuk kreator dan pekerja mobile.',
+ sold: 742,
+ margin: 35,
+ trend: 18,
+ },
+ {
+ id: 'SKU-BE-117',
+ name: 'Lumin Glow Serum',
+ category: 'Beauty',
+ price: 189000,
+ stock: 24,
+ reorderPoint: 20,
+ reorderPack: 80,
+ description: 'Serum wajah lokal dengan bundling campaign affiliate.',
+ sold: 1094,
+ margin: 42,
+ trend: 25,
+ },
+ {
+ id: 'SKU-HO-073',
+ name: 'Bamboo Desk Organizer',
+ category: 'Home Living',
+ price: 159000,
+ stock: 7,
+ reorderPoint: 14,
+ reorderPack: 45,
+ description: 'Organizer meja kerja dari bambu daur ulang.',
+ sold: 286,
+ margin: 31,
+ trend: -4,
+ },
+];
+
+const initialCustomers: Customer[] = [
+ {
+ id: 'CUST-001',
+ name: 'Alya Paramitha',
+ phone: '+62 812 1000 2040',
+ email: 'alya@vorta.demo',
+ address: 'Jakarta Selatan',
+ segment: 'VIP',
+ orders: 18,
+ lifetimeValue: 18450000,
+ },
+ {
+ id: 'CUST-002',
+ name: 'Raka Wijaya',
+ phone: '+62 813 4400 8080',
+ email: 'raka@vorta.demo',
+ address: 'Bandung',
+ segment: 'Reguler',
+ orders: 7,
+ lifetimeValue: 6390000,
+ },
+ {
+ id: 'CUST-003',
+ name: 'Sari Nusantara Mart',
+ phone: '+62 21 5550 9911',
+ email: 'procurement@sarinusa.demo',
+ address: 'Surabaya',
+ segment: 'VIP',
+ orders: 31,
+ lifetimeValue: 52800000,
+ },
+ {
+ id: 'CUST-004',
+ name: 'Dimas Kurnia',
+ phone: '+62 857 9000 1133',
+ email: 'dimas@vorta.demo',
+ address: 'Yogyakarta',
+ segment: 'Baru',
+ orders: 1,
+ lifetimeValue: 1299000,
+ },
+];
+
+const initialOrders: Order[] = [
+ {
+ id: 'ORD-24061',
+ customerId: 'CUST-001',
+ total: 1548000,
+ status: 'Diproses',
+ paymentStatus: 'Berhasil',
+ shipmentStatus: 'Menunggu Pick Up',
+ channel: 'Web Store',
+ createdAt: 'Hari ini, 09:24',
+ items: ['Vorta Smart Speaker X1', 'Nusa Everyday Tote'],
+ },
+ {
+ id: 'ORD-24062',
+ customerId: 'CUST-002',
+ total: 567000,
+ status: 'Baru',
+ paymentStatus: 'Menunggu',
+ shipmentStatus: 'Menunggu Pick Up',
+ channel: 'Marketplace',
+ createdAt: 'Hari ini, 10:02',
+ items: ['Lumin Glow Serum x3'],
+ },
+ {
+ id: 'ORD-24063',
+ customerId: 'CUST-003',
+ total: 7950000,
+ status: 'Dikirim',
+ paymentStatus: 'Berhasil',
+ shipmentStatus: 'Dikirim',
+ channel: 'B2B',
+ createdAt: 'Kemarin, 16:48',
+ items: ['Bamboo Desk Organizer x50'],
+ },
+ {
+ id: 'ORD-24064',
+ customerId: 'CUST-004',
+ total: 1299000,
+ status: 'Baru',
+ paymentStatus: 'Menunggu',
+ shipmentStatus: 'Menunggu Pick Up',
+ channel: 'Social Commerce',
+ createdAt: 'Hari ini, 11:18',
+ items: ['Vorta Smart Speaker X1'],
+ },
+];
+
+const initialPayments: Payment[] = [
+ { id: 'PAY-8801', orderId: 'ORD-24061', method: 'QRIS', amount: 1548000, status: 'Berhasil' },
+ { id: 'PAY-8802', orderId: 'ORD-24062', method: 'VA Bank', amount: 567000, status: 'Menunggu' },
+ { id: 'PAY-8803', orderId: 'ORD-24063', method: 'Kartu Kredit', amount: 7950000, status: 'Berhasil' },
+ { id: 'PAY-8804', orderId: 'ORD-24064', method: 'Wallet', amount: 1299000, status: 'Menunggu' },
+];
+
+const initialShipments: Shipment[] = [
+ {
+ id: 'SHP-7101',
+ orderId: 'ORD-24061',
+ destination: 'Jakarta Selatan',
+ courier: 'Vorta Express',
+ eta: '4 jam',
+ status: 'Menunggu Pick Up',
+ risk: 'Rendah',
+ },
+ {
+ id: 'SHP-7102',
+ orderId: 'ORD-24063',
+ destination: 'Surabaya',
+ courier: 'Partner Cargo',
+ eta: '1 hari',
+ status: 'Dikirim',
+ risk: 'Sedang',
+ },
+ {
+ id: 'SHP-7103',
+ orderId: 'ORD-24064',
+ destination: 'Yogyakarta',
+ courier: 'Instant Fleet',
+ eta: '8 jam',
+ status: 'Menunggu Pick Up',
+ risk: 'Rendah',
+ },
+];
+
+const initialCampaigns: Campaign[] = [
+ {
+ id: 'MKT-501',
+ name: 'Payday Electronics Push',
+ channel: 'Ads',
+ budget: 4500000,
+ status: 'Aktif',
+ conversion: 5.8,
+ revenue: 38250000,
+ },
+ {
+ id: 'MKT-502',
+ name: 'Beauty Affiliate Sprint',
+ channel: 'Affiliate',
+ budget: 2800000,
+ status: 'Draft',
+ conversion: 3.1,
+ revenue: 11450000,
+ },
+ {
+ id: 'MKT-503',
+ name: 'VIP Repeat Buyer Email',
+ channel: 'Email',
+ budget: 650000,
+ status: 'Selesai',
+ conversion: 9.4,
+ revenue: 19750000,
+ },
+];
+
+const initialLedger: LedgerItem[] = [
+ {
+ id: 'LOG-1',
+ title: 'Payment capture berhasil',
+ detail: 'ORD-24061 lunas via QRIS dan siap masuk antrean pick up.',
+ time: '2 menit lalu',
+ tone: 'success',
+ },
+ {
+ id: 'LOG-2',
+ title: 'Low stock alert',
+ detail: 'Nusa Everyday Tote dan Bamboo Desk Organizer di bawah reorder point.',
+ time: '9 menit lalu',
+ tone: 'warning',
+ },
+ {
+ id: 'LOG-3',
+ title: 'Campaign aktif',
+ detail: 'Payday Electronics Push menghasilkan ROAS awal 8.5x.',
+ time: '18 menit lalu',
+ tone: 'info',
+ },
+];
+
+const initialSettings: Settings = {
+ storeName: 'VORTA-COMMERCE HQ',
+ currency: 'IDR',
+ fulfillmentMode: 'Otomatis',
+ lowStockAlert: true,
+ paymentAutoCapture: true,
+};
+
+const tabs: { id: TabId; label: string; iconName: string }[] = [
+ { id: 'dashboard', label: 'Dashboard', iconName: 'mdiViewDashboardOutline' },
+ { id: 'products', label: 'Produk', iconName: 'mdiPackageVariantClosed' },
+ { id: 'inventory', label: 'Inventori', iconName: 'mdiWarehouse' },
+ { id: 'orders', label: 'Pesanan', iconName: 'mdiReceiptTextOutline' },
+ { id: 'customers', label: 'Pelanggan', iconName: 'mdiAccountHeartOutline' },
+ { id: 'payments', label: 'Pembayaran', iconName: 'mdiCreditCardOutline' },
+ { id: 'shipping', label: 'Pengiriman', iconName: 'mdiTruckDeliveryOutline' },
+ { id: 'marketing', label: 'Marketing', iconName: 'mdiBullhornOutline' },
+ { id: 'reports', label: 'Laporan', iconName: 'mdiChartLine' },
+ { id: 'settings', label: 'Pengaturan', iconName: 'mdiCogOutline' },
+];
+
+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').format(value);
+
+const resolveIcon = (iconName: string) =>
+ iconName in icon
+ ? (icon[iconName as keyof typeof icon] as string)
+ : icon.mdiViewDashboardOutline;
+
+const getCustomerName = (customers: Customer[], customerId: string) =>
+ customers.find((customer) => customer.id === customerId)?.name || 'Pelanggan tidak diketahui';
+
+const getStatusClass = (status: string) => {
+ if (['Berhasil', 'Selesai', 'Terkirim', 'Aktif', 'VIP'].includes(status)) {
+ return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-200';
+ }
+
+ if (['Menunggu', 'Baru', 'Menunggu Pick Up', 'Draft', 'Reguler'].includes(status)) {
+ return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-200';
+ }
+
+ if (['Gagal', 'Refund', 'Tahan QC'].includes(status)) {
+ return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-200';
+ }
+
+ return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-200';
+};
+
+const getToneClass = (tone: LedgerItem['tone']) => {
+ if (tone === 'success') return 'bg-emerald-500';
+ if (tone === 'warning') return 'bg-amber-500';
+ if (tone === 'danger') return 'bg-red-500';
+
+ return 'bg-blue-500';
+};
+
+const VortaCommercePage = () => {
+ const { currentUser } = useAppSelector((state) => state.auth);
+ const [activeTab, setActiveTab] = useState('dashboard');
+ const [products, setProducts] = useState(initialProducts);
+ const [customers, setCustomers] = useState(initialCustomers);
+ const [orders, setOrders] = useState(initialOrders);
+ const [payments, setPayments] = useState(initialPayments);
+ const [shipments, setShipments] = useState(initialShipments);
+ const [campaigns, setCampaigns] = useState(initialCampaigns);
+ const [ledger, setLedger] = useState(initialLedger);
+ const [settings, setSettings] = useState(initialSettings);
+
+ const metrics = useMemo(() => {
+ const paidRevenue = payments
+ .filter((payment) => payment.status === 'Berhasil')
+ .reduce((total, payment) => total + payment.amount, 0);
+ const pendingRevenue = payments
+ .filter((payment) => payment.status === 'Menunggu')
+ .reduce((total, payment) => total + payment.amount, 0);
+ const inventoryValue = products.reduce(
+ (total, product) => total + product.price * product.stock,
+ 0,
+ );
+ const lowStockProducts = products.filter(
+ (product) => product.stock <= product.reorderPoint,
+ );
+ const completedOrders = orders.filter((order) => order.status === 'Selesai').length;
+ const successPaymentCount = payments.filter((payment) => payment.status === 'Berhasil').length;
+
+ return {
+ paidRevenue,
+ pendingRevenue,
+ inventoryValue,
+ lowStockCount: lowStockProducts.length,
+ lowStockProducts,
+ openOrders: orders.filter((order) => order.status !== 'Selesai').length,
+ completedOrders,
+ totalOrders: orders.length,
+ averageOrderValue: orders.length
+ ? orders.reduce((total, order) => total + order.total, 0) / orders.length
+ : 0,
+ paymentSuccessRate: payments.length ? Math.round((successPaymentCount / payments.length) * 100) : 0,
+ totalCustomers: customers.length,
+ activeCampaigns: campaigns.filter((campaign) => campaign.status === 'Aktif').length,
+ marketingRevenue: campaigns.reduce((total, campaign) => total + campaign.revenue, 0),
+ totalStock: products.reduce((total, product) => total + product.stock, 0),
+ };
+ }, [campaigns, customers.length, orders, payments, products]);
+
+ const bestSeller = useMemo(
+ () => [...products].sort((first, second) => second.sold - first.sold)[0],
+ [products],
+ );
+
+ const pushLedger = (title: string, detail: string, tone: LedgerItem['tone'] = 'info') => {
+ setLedger((current) => [
+ {
+ id: `LOG-${Date.now()}`,
+ title,
+ detail,
+ time: 'Baru saja',
+ tone,
+ },
+ ...current,
+ ].slice(0, 8));
+ };
+
+ const restockLowStock = () => {
+ const lowStockProducts = products.filter((product) => product.stock <= product.reorderPoint);
+
+ if (!lowStockProducts.length) {
+ pushLedger('Inventori aman', 'Tidak ada produk di bawah reorder point saat ini.', 'success');
+ return;
+ }
+
+ setProducts((current) =>
+ current.map((product) =>
+ product.stock <= product.reorderPoint
+ ? { ...product, stock: product.stock + product.reorderPack }
+ : product,
+ ),
+ );
+ pushLedger(
+ 'Auto-restock dibuat',
+ `${lowStockProducts.length} SKU masuk purchase order otomatis dari inventori.`,
+ 'success',
+ );
+ };
+
+ const createFlashOrder = () => {
+ const product = products.find((item) => item.id === 'SKU-BE-117') || products[0];
+ const quantity = 3;
+
+ if (product.stock < quantity) {
+ pushLedger('Order gagal dibuat', `${product.name} tidak punya stok cukup.`, 'danger');
+ return;
+ }
+
+ const orderNumber = orders.length + 24061;
+ const paymentNumber = payments.length + 8801;
+ const shipmentNumber = shipments.length + 7101;
+ const total = product.price * quantity + 25000;
+ const nextOrder: Order = {
+ id: `ORD-${orderNumber}`,
+ customerId: 'CUST-002',
+ total,
+ status: 'Baru',
+ paymentStatus: 'Menunggu',
+ shipmentStatus: 'Menunggu Pick Up',
+ channel: 'Social Commerce',
+ createdAt: 'Baru saja',
+ items: [`${product.name} x${quantity}`],
+ };
+
+ setProducts((current) =>
+ current.map((item) =>
+ item.id === product.id
+ ? { ...item, stock: item.stock - quantity, sold: item.sold + quantity }
+ : item,
+ ),
+ );
+ setOrders((current) => [nextOrder, ...current]);
+ setPayments((current) => [
+ {
+ id: `PAY-${paymentNumber}`,
+ orderId: nextOrder.id,
+ method: 'QRIS',
+ amount: total,
+ status: 'Menunggu',
+ },
+ ...current,
+ ]);
+ setShipments((current) => [
+ {
+ id: `SHP-${shipmentNumber}`,
+ orderId: nextOrder.id,
+ destination: 'Bandung',
+ courier: 'Vorta Express',
+ eta: '6 jam',
+ status: 'Menunggu Pick Up',
+ risk: 'Rendah',
+ },
+ ...current,
+ ]);
+ setCustomers((current) =>
+ current.map((customer) =>
+ customer.id === 'CUST-002'
+ ? {
+ ...customer,
+ orders: customer.orders + 1,
+ lifetimeValue: customer.lifetimeValue + total,
+ }
+ : customer,
+ ),
+ );
+ pushLedger('Flash order dibuat', `${nextOrder.id} dibuat dari Social Commerce.`, 'success');
+ };
+
+ const capturePayment = (paymentId: string) => {
+ const payment = payments.find((item) => item.id === paymentId);
+
+ if (!payment || payment.status === 'Berhasil') return;
+
+ setPayments((current) =>
+ current.map((item) => (item.id === paymentId ? { ...item, status: 'Berhasil' } : item)),
+ );
+ setOrders((current) =>
+ current.map((order) =>
+ order.id === payment.orderId
+ ? { ...order, paymentStatus: 'Berhasil', status: order.status === 'Baru' ? 'Diproses' : order.status }
+ : order,
+ ),
+ );
+ pushLedger('Pembayaran sukses', `${payment.orderId} ditandai lunas via ${payment.method}.`, 'success');
+ };
+
+ const advanceOrder = (orderId: string) => {
+ const order = orders.find((item) => item.id === orderId);
+
+ if (!order) return;
+
+ const nextStatus: Record = {
+ Baru: 'Diproses',
+ Diproses: 'Dikirim',
+ Dikirim: 'Selesai',
+ Selesai: 'Selesai',
+ };
+ const next = nextStatus[order.status];
+
+ setOrders((current) =>
+ current.map((item) =>
+ item.id === orderId
+ ? {
+ ...item,
+ status: next,
+ shipmentStatus:
+ next === 'Dikirim' ? 'Dikirim' : next === 'Selesai' ? 'Terkirim' : item.shipmentStatus,
+ }
+ : item,
+ ),
+ );
+ setShipments((current) =>
+ current.map((shipment) =>
+ shipment.orderId === orderId
+ ? {
+ ...shipment,
+ status:
+ next === 'Dikirim' ? 'Dikirim' : next === 'Selesai' ? 'Terkirim' : shipment.status,
+ }
+ : shipment,
+ ),
+ );
+ pushLedger('Status pesanan diperbarui', `${orderId} berpindah ke status ${next}.`, 'info');
+ };
+
+ const completeShipment = (shipmentId: string) => {
+ const shipment = shipments.find((item) => item.id === shipmentId);
+
+ if (!shipment || shipment.status === 'Terkirim') return;
+
+ setShipments((current) =>
+ current.map((item) => (item.id === shipmentId ? { ...item, status: 'Terkirim', eta: 'Selesai' } : item)),
+ );
+ setOrders((current) =>
+ current.map((order) =>
+ order.id === shipment.orderId
+ ? { ...order, status: 'Selesai', shipmentStatus: 'Terkirim' }
+ : order,
+ ),
+ );
+ pushLedger('Paket terkirim', `${shipment.orderId} selesai oleh ${shipment.courier}.`, 'success');
+ };
+
+ const launchCampaign = (campaignId: string) => {
+ const campaign = campaigns.find((item) => item.id === campaignId);
+
+ if (!campaign) return;
+
+ setCampaigns((current) =>
+ current.map((item) =>
+ item.id === campaignId
+ ? {
+ ...item,
+ status: 'Aktif',
+ conversion: Number((item.conversion + 0.9).toFixed(1)),
+ revenue: item.revenue + Math.round(item.budget * 2.4),
+ }
+ : item,
+ ),
+ );
+ pushLedger('Campaign diluncurkan', `${campaign.name} aktif dengan budget ${formatCurrency(campaign.budget)}.`, 'success');
+ };
+
+ const closeDailyReport = () => {
+ pushLedger(
+ 'Laporan harian dikunci',
+ `${orders.length} pesanan, ${formatCurrency(metrics.paidRevenue)} revenue lunas, ${metrics.lowStockCount} SKU perlu perhatian.`,
+ 'info',
+ );
+ };
+
+ const toggleFulfillmentMode = () => {
+ setSettings((current) => ({
+ ...current,
+ fulfillmentMode: current.fulfillmentMode === 'Otomatis' ? 'Manual Review' : 'Otomatis',
+ }));
+ pushLedger('Pengaturan fulfillment berubah', 'Mode fulfillment toko diperbarui.', 'info');
+ };
+
+ const toggleBooleanSetting = (key: 'lowStockAlert' | 'paymentAutoCapture') => {
+ setSettings((current) => ({ ...current, [key]: !current[key] }));
+ };
+
+ const renderMetricCard = (label: string, value: string, helper: string, iconName: string, accent: string) => (
+
+
+
+
{label}
+
{value}
+
{helper}
+
+
+
+
+
+
+ );
+
+ const renderDashboard = () => (
+
+
+ {renderMetricCard(
+ 'Revenue lunas',
+ formatCurrency(metrics.paidRevenue),
+ `${metrics.paymentSuccessRate}% payment success rate`,
+ 'mdiCashMultiple',
+ 'bg-emerald-500',
+ )}
+ {renderMetricCard(
+ 'Open orders',
+ formatNumber(metrics.openOrders),
+ `${metrics.completedOrders} pesanan selesai`,
+ 'mdiReceiptClockOutline',
+ 'bg-blue-500',
+ )}
+ {renderMetricCard(
+ 'Nilai inventori',
+ formatCurrency(metrics.inventoryValue),
+ `${formatNumber(metrics.totalStock)} unit aktif`,
+ 'mdiWarehouse',
+ 'bg-violet-500',
+ )}
+ {renderMetricCard(
+ 'Low stock',
+ formatNumber(metrics.lowStockCount),
+ metrics.lowStockCount ? 'Perlu reorder otomatis' : 'Semua SKU aman',
+ 'mdiAlertCircleOutline',
+ metrics.lowStockCount ? 'bg-amber-500' : 'bg-emerald-500',
+ )}
+
+
+
+
+
+
+
Command Center Commerce
+
+ Ringkasan real-time untuk produk, inventori, order, payment, dan pengiriman.
+
+
+
+ createFlashOrder()}
+ />
+ restockLowStock()}
+ />
+
+
+
+
+
+
AOV
+
{formatCurrency(metrics.averageOrderValue)}
+
Average order value hari ini
+
+
+
Pending payment
+
{formatCurrency(metrics.pendingRevenue)}
+
Belum masuk cash-in
+
+
+
Marketing revenue
+
{formatCurrency(metrics.marketingRevenue)}
+
Dari {metrics.activeCampaigns} campaign aktif
+
+
+
+
+
+
+
+ Produk terlaris
+
+
{bestSeller?.name}
+
+ {formatNumber(bestSeller?.sold || 0)} unit terjual • margin {bestSeller?.margin || 0}% • trend {bestSeller?.trend || 0}%
+
+
+
setActiveTab('products')}
+ />
+
+
+
+
+
+
+
+
Live Activity
+
Event terbaru sistem
+
+
+
+
+ {ledger.slice(0, 5).map((item) => (
+
+
+
+
{item.title}
+
{item.detail}
+
{item.time}
+
+
+ ))}
+
+
+
+
+ );
+
+ const renderProducts = () => (
+
+
+
+
Produk
+
Katalog produk utama seperti tabel `products`.
+
+
createFlashOrder()}
+ />
+
+
+
+
+
+ Produk
+ Kategori
+ Harga
+ Stok
+ Terjual
+ Margin
+
+
+
+ {products.map((product) => (
+
+
+ {product.name}
+ {product.id} • {product.description}
+
+ {product.category}
+ {formatCurrency(product.price)}
+
+
+ {formatNumber(product.stock)} unit
+
+
+ {formatNumber(product.sold)}
+
+ = 0 ? 'text-emerald-600' : 'text-red-500'}>
+ {product.margin}% • {product.trend >= 0 ? '+' : ''}{product.trend}%
+
+
+
+ ))}
+
+
+
+
+ );
+
+ const renderInventory = () => (
+
+
+
+
+
Inventori
+
Monitor stok, reorder point, dan nilai gudang.
+
+
restockLowStock()}
+ />
+
+
+ {products.map((product) => {
+ const stockRatio = Math.min(100, Math.round((product.stock / (product.reorderPoint + product.reorderPack)) * 100));
+ return (
+
+
+
+
{product.name}
+
+ Stok {formatNumber(product.stock)} • Reorder point {product.reorderPoint} • Pack {product.reorderPack}
+
+
+
+ {product.stock <= product.reorderPoint ? 'Perlu Reorder' : 'Aman'}
+
+
+
+
+ );
+ })}
+
+
+
+ Purchase Signal
+ SKU prioritas untuk reorder.
+
+ {metrics.lowStockProducts.length ? (
+ metrics.lowStockProducts.map((product) => (
+
+
{product.name}
+
+ Rekomendasi beli {product.reorderPack} unit. Estimasi nilai {formatCurrency(product.reorderPack * product.price)}.
+
+
+ ))
+ ) : (
+
+ Semua SKU berada di atas reorder point.
+
+ )}
+
+
+
+ );
+
+ const renderOrders = () => (
+
+
+
+
Pesanan
+
Pipeline dari order baru sampai selesai.
+
+
createFlashOrder()}
+ />
+
+
+ {orders.map((order) => (
+
+
+
+
+
{order.id}
+
{order.status}
+
Payment {order.paymentStatus}
+
+
+ {getCustomerName(customers, order.customerId)} • {order.channel} • {order.createdAt}
+
+
{order.items.join(', ')}
+
+
+
{formatCurrency(order.total)}
+
advanceOrder(order.id)}
+ />
+
+
+
+ ))}
+
+
+ );
+
+ const renderCustomers = () => (
+
+
+
Pelanggan
+
CRM sederhana untuk segmentasi VIP, reguler, dan pelanggan baru.
+
+
+ {customers.map((customer) => (
+
+
+
+
{customer.name}
+
{customer.email} • {customer.phone}
+
{customer.address}
+
+
{customer.segment}
+
+
+
+
Total order
+
{formatNumber(customer.orders)}
+
+
+
Lifetime value
+
{formatCurrency(customer.lifetimeValue)}
+
+
+
+ ))}
+
+
+ );
+
+ const renderPayments = () => (
+
+
+
Pembayaran
+
Payment capture, status settlement, dan metode transaksi.
+
+
+ {payments.map((payment) => (
+
+
+
+
+
{payment.id}
+
{payment.status}
+
+
+ {payment.orderId} • {payment.method} • {formatCurrency(payment.amount)}
+
+
+
capturePayment(payment.id)}
+ />
+
+
+ ))}
+
+
+ );
+
+ const renderShipping = () => (
+
+
+
Pengiriman
+
Fulfillment, courier, ETA, dan risiko pengiriman.
+
+
+ {shipments.map((shipment) => (
+
+
+
+
+
{shipment.id}
+
{shipment.status}
+
+
+ {shipment.orderId} • {shipment.destination} • {shipment.courier}
+
+
ETA {shipment.eta} • Risiko {shipment.risk}
+
+
completeShipment(shipment.id)}
+ />
+
+
+ ))}
+
+
+ );
+
+ const renderMarketing = () => (
+
+
+
Marketing
+
Campaign, channel, budget, conversion, dan revenue.
+
+
+ {campaigns.map((campaign) => (
+
+
+
+
{campaign.name}
+
{campaign.channel} • {campaign.id}
+
+
{campaign.status}
+
+
+
Budget {formatCurrency(campaign.budget)}
+
Conversion {campaign.conversion}%
+
Revenue {formatCurrency(campaign.revenue)}
+
+
launchCampaign(campaign.id)}
+ />
+
+ ))}
+
+
+ );
+
+ const renderReports = () => {
+ const paidOrders = orders.filter((order) => order.paymentStatus === 'Berhasil').length;
+ const shippedOrders = orders.filter((order) => order.shipmentStatus === 'Terkirim').length;
+ const roas = campaigns.length
+ ? campaigns.reduce((total, campaign) => total + campaign.revenue, 0) /
+ campaigns.reduce((total, campaign) => total + campaign.budget, 0)
+ : 0;
+
+ return (
+
+
+
+
+
Laporan Eksekutif
+
Snapshot performa commerce.
+
+
closeDailyReport()}
+ />
+
+
+
+ Revenue lunas
+ {formatCurrency(metrics.paidRevenue)}
+
+
+ Paid orders
+ {paidOrders}/{orders.length}
+
+
+ Delivered orders
+ {shippedOrders}/{orders.length}
+
+
+ Marketing ROAS
+ {roas.toFixed(1)}x
+
+
+
+
+ API & Database Mapping
+
+ MVP ini mengikuti struktur yang kamu minta dan siap dinaikkan ke Sequelize migration/API permanen.
+
+
+ {[
+ 'users',
+ 'products',
+ 'inventory',
+ 'customers',
+ 'orders',
+ 'payments',
+ 'shipping',
+ 'marketing',
+ 'reports',
+ 'dashboard',
+ ].map((moduleName) => (
+
+ /api/{moduleName}
+
+ ))}
+
+
+
+ );
+ };
+
+ const renderSettings = () => (
+
+
+ Pengaturan Toko
+ Konfigurasi operasional aplikasi utama.
+
+
+
Nama toko
+
{settings.storeName}
+
+
+
Mata uang
+
{settings.currency}
+
+
+
Fulfillment mode
+
+
{settings.fulfillmentMode}
+
toggleFulfillmentMode()}
+ />
+
+
+
+
+
+ Automation
+ Kontrol rule otomatis untuk commerce operations.
+
+
+
+
Low stock alert
+
Kirim peringatan saat SKU di bawah reorder point.
+
+
toggleBooleanSetting('lowStockAlert')}
+ />
+
+
+
+
Payment auto-capture
+
Otomatis capture payment setelah callback gateway valid.
+
+
toggleBooleanSetting('paymentAutoCapture')}
+ />
+
+
+
+
+ );
+
+ const renderActiveTab = () => {
+ if (activeTab === 'products') return renderProducts();
+ if (activeTab === 'inventory') return renderInventory();
+ if (activeTab === 'orders') return renderOrders();
+ if (activeTab === 'customers') return renderCustomers();
+ if (activeTab === 'payments') return renderPayments();
+ if (activeTab === 'shipping') return renderShipping();
+ if (activeTab === 'marketing') return renderMarketing();
+ if (activeTab === 'reports') return renderReports();
+ if (activeTab === 'settings') return renderSettings();
+
+ return renderDashboard();
+ };
+
+ return (
+ <>
+
+ {getPageTitle('VORTA-COMMERCE')}
+
+
+
+
+
+
+
+
+
+
Commerce Operating System
+
Dashboard utama untuk produk, order, pembayaran, dan fulfillment.
+
+ MVP ini menyatukan frontend, API module, dan rancangan database VORTA-COMMERCE dalam satu command center yang langsung bisa diuji.
+
+
+ createFlashOrder()}
+ />
+ restockLowStock()}
+ />
+ setActiveTab('reports')}
+ />
+
+
+
+
Operator
+
+ {currentUser?.firstName || currentUser?.email || 'Admin VORTA'}
+
+
+
Store {settings.storeName}
+
Mode {settings.fulfillmentMode}
+
Open orders {metrics.openOrders}
+
Low stock {metrics.lowStockCount}
+
+
+
+
+
+
+ {tabs.map((tab) => (
+ setActiveTab(tab.id)}
+ className={`inline-flex flex-none items-center gap-2 rounded-2xl border px-4 py-2 text-sm font-semibold transition-colors ${
+ activeTab === tab.id
+ ? 'border-blue-500 bg-blue-500 text-white shadow-sm'
+ : 'border-slate-200 bg-white text-slate-700 hover:border-blue-300 hover:text-blue-600 dark:border-dark-700 dark:bg-dark-900 dark:text-slate-200'
+ }`}
+ >
+
+ {tab.label}
+
+ ))}
+
+
+ {renderActiveTab()}
+
+ >
+ );
+};
+
+VortaCommercePage.getLayout = function getLayout(page: ReactElement) {
+ return {page} ;
+};
+
+export default VortaCommercePage;
diff --git a/frontend/src/pages/vorta-synapse.tsx b/frontend/src/pages/vorta-synapse.tsx
new file mode 100644
index 0000000..87570fc
--- /dev/null
+++ b/frontend/src/pages/vorta-synapse.tsx
@@ -0,0 +1,800 @@
+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([
+ {
+ 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([
+ {
+ 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([
+ {
+ 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([
+ {
+ 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([
+ {
+ 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 (
+ <>
+
+ {getPageTitle('04 Vorta Synapse')}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Protokol distribusi, afiliasi, dan logistik pintar untuk ekonomi kreator
+
+
+ Halo {userName}, sinkronkan drop kreator dari kampanye sampai paket terkirim.
+
+
+ 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.
+
+
+ {[
+ { 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) => (
+
+
+
+ {item.label}
+
+
{item.value}
+
+ ))}
+
+
+
+
+
+
+
Synapse health
+
{routingEfficiency}% route efficiency
+
+
+ Protocol Live
+
+
+
+ {ledger.slice(0, 4).map((event) => (
+
+
+ {event.module}
+ {event.time}
+
+
{event.title}
+
{event.detail}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
Node aktif
+
+
+
{activeNode.name}
+
+ {activeNode.type} di {activeNode.city} • kapasitas {activeNode.capacity}% terpakai
+
+
+
+
+
+
+
+
+
Health score
+
{activeNode.health}%
+
+ Node siap menerima batch dan menjaga SLA fulfillment.
+
+
+
+
Routing efficiency
+
{routingEfficiency}%
+
+ Optimasi mempertimbangkan stok, risiko, jarak, dan kapasitas mitra.
+
+
+
+
Average commission
+
{averageCommission}%
+
+ Tarif dinamis mengikuti performa dan kualitas traffic partner.
+
+
+
+
+
+
+
+
Launcher kampanye kreator
+
+ Buat batch distribusi baru dan isi pool afiliasi otomatis.
+
+
+
+
+
+ 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"
+ />
+ 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"
+ />
+
+
+
+ Template margin mengikuti {selectedDrop.title} ({selectedDrop.margin}%).
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Komisi Afiliasi
+
+ Pool komisi untuk kreator, komunitas, dan reseller.
+
+
+
+
+
+
Pool siap settlement
+
{formatCurrency(commissionPool)}
+
+ Partner aktif: {partners.filter((partner) => partner.status === 'Aktif').length}/{partners.length}
+
+
+
+
+ {partners.map((partner) => (
+
+
+
+
{partner.name}
+
{partner.tier}
+
+
+ {partner.status}
+
+
+
+
+
Sales
+
{formatNumber(partner.sales)}
+
+
+
Rate
+
{partner.commissionRate}%
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
Drop Kreator
+
+ Produk digital/fisik yang sedang didorong oleh protokol distribusi.
+
+
+
+
+
+ {drops.map((drop) => {
+ const progress = Math.round((drop.sold / drop.inventory) * 100);
+
+ return (
+
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'
+ }`}
+ >
+
+ Margin {drop.margin}%
+
+ {drop.title}
+
+ {drop.creator} • {drop.channel}
+
+
+ {formatNumber(drop.sold)} / {formatNumber(drop.inventory)} terjual
+ {progress}%
+
+
+
+ );
+ })}
+
+
+
+
+
Boost afiliasi terpilih
+
{selectedDrop.title}
+
+
+
+
+ Simulasi ini menambah proyeksi penjualan dan menaikkan margin drop pilihan.
+
+
+
+
+
+
+
+
+
+
Logistik Pintar
+
+ Queue pengiriman dengan ETA, risiko, dan aksi penyelesaian.
+
+
+
+
+
+ {jobs.map((job) => (
+
+
+
+
+
{job.code}
+
+ Risiko {job.risk}
+
+
+
+ {job.destination} • {job.courier}
+
+
+
+
{job.status}
+
ETA {job.eta}
+
+
+
dispatchShipment(job.id)}
+ />
+
+ ))}
+
+
+
+
+
+ >
+ );
+};
+
+VortaSynapsePage.getLayout = function getLayout(page: ReactElement) {
+ return {page} ;
+};
+
+export default VortaSynapsePage;