Autosave: 20260619-030652

This commit is contained in:
Flatlogic Bot 2026-06-19 03:06:49 +00:00
parent 0925ce59ce
commit f954e3c153
18 changed files with 5061 additions and 259 deletions

View File

@ -0,0 +1,133 @@
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
const tables = await queryInterface.showAllTables({ transaction });
const tableNames = tables.map((table) => (typeof table === 'object' ? table.tableName : table));
if (!tableNames.includes('vorta_commerce_states')) {
await queryInterface.createTable(
'vorta_commerce_states',
{
id: {
type: Sequelize.DataTypes.UUID,
defaultValue: Sequelize.DataTypes.UUIDV4,
primaryKey: true,
},
userId: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
storeId: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'stores',
key: 'id',
},
},
organizationsId: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'organizations',
key: 'id',
},
},
settings: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
campaigns: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: [],
},
ledger: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: [],
},
product_meta: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
customer_meta: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
order_meta: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
shipment_meta: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
importHash: {
type: Sequelize.DataTypes.STRING(255),
allowNull: true,
unique: true,
},
createdById: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
updatedById: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
createdAt: {
type: Sequelize.DataTypes.DATE,
},
updatedAt: {
type: Sequelize.DataTypes.DATE,
},
deletedAt: {
type: Sequelize.DataTypes.DATE,
},
},
{ transaction },
);
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
console.error('Failed to create vorta_commerce_states table:', error);
throw error;
}
},
async down(queryInterface) {
const transaction = await queryInterface.sequelize.transaction();
try {
const tables = await queryInterface.showAllTables({ transaction });
const tableNames = tables.map((table) => (typeof table === 'object' ? table.tableName : table));
if (tableNames.includes('vorta_commerce_states')) {
await queryInterface.dropTable('vorta_commerce_states', { transaction });
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
console.error('Failed to drop vorta_commerce_states table:', error);
throw error;
}
},
};

View File

@ -0,0 +1,116 @@
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
const tables = await queryInterface.showAllTables({ transaction });
const tableNames = tables.map((table) => (typeof table === 'object' ? table.tableName : table));
if (!tableNames.includes('vorta_universe_states')) {
await queryInterface.createTable(
'vorta_universe_states',
{
id: {
type: Sequelize.DataTypes.UUID,
defaultValue: Sequelize.DataTypes.UUIDV4,
primaryKey: true,
},
userId: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
organizationsId: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'organizations',
key: 'id',
},
},
profile: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
modules: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: [],
},
products: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: [],
},
ledger: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: [],
},
settings: {
type: Sequelize.DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
importHash: {
type: Sequelize.DataTypes.STRING(255),
allowNull: true,
unique: true,
},
createdById: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
updatedById: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
createdAt: {
type: Sequelize.DataTypes.DATE,
},
updatedAt: {
type: Sequelize.DataTypes.DATE,
},
deletedAt: {
type: Sequelize.DataTypes.DATE,
},
},
{ transaction },
);
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
console.error('Failed to create vorta_universe_states table:', error);
throw error;
}
},
async down(queryInterface) {
const transaction = await queryInterface.sequelize.transaction();
try {
const tables = await queryInterface.showAllTables({ transaction });
const tableNames = tables.map((table) => (typeof table === 'object' ? table.tableName : table));
if (tableNames.includes('vorta_universe_states')) {
await queryInterface.dropTable('vorta_universe_states', { transaction });
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
console.error('Failed to drop vorta_universe_states table:', error);
throw error;
}
},
};

View File

@ -0,0 +1,116 @@
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
const tables = await queryInterface.showAllTables({ transaction });
const tableNames = tables.map((table) => (typeof table === 'object' ? table.tableName : table));
if (!tableNames.includes('vorta_login_otps')) {
await queryInterface.createTable(
'vorta_login_otps',
{
id: {
type: Sequelize.DataTypes.UUID,
defaultValue: Sequelize.DataTypes.UUIDV4,
primaryKey: true,
},
userId: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
organizationsId: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'organizations',
key: 'id',
},
},
phone: {
type: Sequelize.DataTypes.STRING(40),
allowNull: false,
},
otpHash: {
type: Sequelize.DataTypes.STRING(255),
allowNull: false,
},
status: {
type: Sequelize.DataTypes.STRING(40),
allowNull: false,
defaultValue: 'pending',
},
attempts: {
type: Sequelize.DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
expiresAt: {
type: Sequelize.DataTypes.DATE,
allowNull: false,
},
verifiedAt: {
type: Sequelize.DataTypes.DATE,
},
importHash: {
type: Sequelize.DataTypes.STRING(255),
allowNull: true,
unique: true,
},
createdById: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
updatedById: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
createdAt: {
type: Sequelize.DataTypes.DATE,
},
updatedAt: {
type: Sequelize.DataTypes.DATE,
},
deletedAt: {
type: Sequelize.DataTypes.DATE,
},
},
{ transaction },
);
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
console.error('Failed to create vorta_login_otps table:', error);
throw error;
}
},
async down(queryInterface) {
const transaction = await queryInterface.sequelize.transaction();
try {
const tables = await queryInterface.showAllTables({ transaction });
const tableNames = tables.map((table) => (typeof table === 'object' ? table.tableName : table));
if (tableNames.includes('vorta_login_otps')) {
await queryInterface.dropTable('vorta_login_otps', { transaction });
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
console.error('Failed to drop vorta_login_otps table:', error);
throw error;
}
},
};

View File

@ -0,0 +1,144 @@
module.exports = {
async up(queryInterface, Sequelize) {
const transaction = await queryInterface.sequelize.transaction();
try {
const tables = await queryInterface.showAllTables({ transaction });
const tableNames = tables.map((table) => (typeof table === 'object' ? table.tableName : table));
if (!tableNames.includes('vorta_social_posts')) {
await queryInterface.createTable(
'vorta_social_posts',
{
id: {
type: Sequelize.DataTypes.UUID,
defaultValue: Sequelize.DataTypes.UUIDV4,
primaryKey: true,
},
userId: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
organizationsId: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'organizations',
key: 'id',
},
},
user: {
type: Sequelize.DataTypes.STRING(255),
allowNull: false,
},
content: {
type: Sequelize.DataTypes.TEXT,
allowNull: false,
},
created_at: {
type: Sequelize.DataTypes.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
importHash: {
type: Sequelize.DataTypes.STRING(255),
allowNull: true,
unique: true,
},
},
{ transaction },
);
}
if (!tableNames.includes('vorta_social_comments')) {
await queryInterface.createTable(
'vorta_social_comments',
{
id: {
type: Sequelize.DataTypes.UUID,
defaultValue: Sequelize.DataTypes.UUIDV4,
primaryKey: true,
},
post_id: {
type: Sequelize.DataTypes.UUID,
allowNull: false,
references: {
model: 'vorta_social_posts',
key: 'id',
},
},
userId: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'users',
key: 'id',
},
},
organizationsId: {
type: Sequelize.DataTypes.UUID,
references: {
model: 'organizations',
key: 'id',
},
},
user: {
type: Sequelize.DataTypes.STRING(255),
allowNull: false,
},
comment: {
type: Sequelize.DataTypes.TEXT,
allowNull: false,
},
parent_id: {
type: Sequelize.DataTypes.STRING(255),
allowNull: false,
defaultValue: '0',
},
created_at: {
type: Sequelize.DataTypes.DATE,
allowNull: false,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP'),
},
importHash: {
type: Sequelize.DataTypes.STRING(255),
allowNull: true,
unique: true,
},
},
{ transaction },
);
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
console.error('Failed to create VORTA social feed tables:', error);
throw error;
}
},
async down(queryInterface) {
const transaction = await queryInterface.sequelize.transaction();
try {
const tables = await queryInterface.showAllTables({ transaction });
const tableNames = tables.map((table) => (typeof table === 'object' ? table.tableName : table));
if (tableNames.includes('vorta_social_comments')) {
await queryInterface.dropTable('vorta_social_comments', { transaction });
}
if (tableNames.includes('vorta_social_posts')) {
await queryInterface.dropTable('vorta_social_posts', { transaction });
}
await transaction.commit();
} catch (error) {
await transaction.rollback();
console.error('Failed to drop VORTA social feed tables:', error);
throw error;
}
},
};

View File

@ -0,0 +1,93 @@
module.exports = function(sequelize, DataTypes) {
const vorta_commerce_states = sequelize.define(
'vorta_commerce_states',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
settings: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
campaigns: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: [],
},
ledger: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: [],
},
product_meta: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
customer_meta: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
order_meta: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
shipment_meta: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
unique: true,
},
},
{
timestamps: true,
paranoid: true,
freezeTableName: true,
},
);
vorta_commerce_states.associate = (db) => {
db.vorta_commerce_states.belongsTo(db.users, {
as: 'user',
foreignKey: {
name: 'userId',
},
constraints: false,
});
db.vorta_commerce_states.belongsTo(db.stores, {
as: 'store',
foreignKey: {
name: 'storeId',
},
constraints: false,
});
db.vorta_commerce_states.belongsTo(db.organizations, {
as: 'organizations',
foreignKey: {
name: 'organizationsId',
},
constraints: false,
});
db.vorta_commerce_states.belongsTo(db.users, {
as: 'createdBy',
});
db.vorta_commerce_states.belongsTo(db.users, {
as: 'updatedBy',
});
};
return vorta_commerce_states;
};

View File

@ -0,0 +1,75 @@
module.exports = function(sequelize, DataTypes) {
const vorta_login_otps = sequelize.define(
'vorta_login_otps',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
phone: {
type: DataTypes.STRING(40),
allowNull: false,
},
otpHash: {
type: DataTypes.STRING(255),
allowNull: false,
},
status: {
type: DataTypes.STRING(40),
allowNull: false,
defaultValue: 'pending',
},
attempts: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0,
},
expiresAt: {
type: DataTypes.DATE,
allowNull: false,
},
verifiedAt: {
type: DataTypes.DATE,
},
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
unique: true,
},
},
{
timestamps: true,
paranoid: true,
freezeTableName: true,
},
);
vorta_login_otps.associate = (db) => {
db.vorta_login_otps.belongsTo(db.users, {
as: 'user',
foreignKey: {
name: 'userId',
},
constraints: false,
});
db.vorta_login_otps.belongsTo(db.organizations, {
as: 'organizations',
foreignKey: {
name: 'organizationsId',
},
constraints: false,
});
db.vorta_login_otps.belongsTo(db.users, {
as: 'createdBy',
});
db.vorta_login_otps.belongsTo(db.users, {
as: 'updatedBy',
});
};
return vorta_login_otps;
};

View File

@ -0,0 +1,71 @@
module.exports = function(sequelize, DataTypes) {
const vorta_social_comments = sequelize.define(
'vorta_social_comments',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
post_id: {
type: DataTypes.UUID,
allowNull: false,
},
user: {
type: DataTypes.STRING(255),
allowNull: false,
},
comment: {
type: DataTypes.TEXT,
allowNull: false,
},
parent_id: {
type: DataTypes.STRING(255),
allowNull: false,
defaultValue: '0',
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
unique: true,
},
},
{
timestamps: false,
freezeTableName: true,
},
);
vorta_social_comments.associate = (db) => {
db.vorta_social_comments.belongsTo(db.vorta_social_posts, {
as: 'post',
foreignKey: {
name: 'post_id',
},
constraints: false,
});
db.vorta_social_comments.belongsTo(db.users, {
as: 'account',
foreignKey: {
name: 'userId',
},
constraints: false,
});
db.vorta_social_comments.belongsTo(db.organizations, {
as: 'organizations',
foreignKey: {
name: 'organizationsId',
},
constraints: false,
});
};
return vorta_social_comments;
};

View File

@ -0,0 +1,62 @@
module.exports = function(sequelize, DataTypes) {
const vorta_social_posts = sequelize.define(
'vorta_social_posts',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
user: {
type: DataTypes.STRING(255),
allowNull: false,
},
content: {
type: DataTypes.TEXT,
allowNull: false,
},
created_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
},
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
unique: true,
},
},
{
timestamps: false,
freezeTableName: true,
},
);
vorta_social_posts.associate = (db) => {
db.vorta_social_posts.hasMany(db.vorta_social_comments, {
as: 'comments',
foreignKey: {
name: 'post_id',
},
constraints: false,
});
db.vorta_social_posts.belongsTo(db.users, {
as: 'account',
foreignKey: {
name: 'userId',
},
constraints: false,
});
db.vorta_social_posts.belongsTo(db.organizations, {
as: 'organizations',
foreignKey: {
name: 'organizationsId',
},
constraints: false,
});
};
return vorta_social_posts;
};

View File

@ -0,0 +1,75 @@
module.exports = function(sequelize, DataTypes) {
const vorta_universe_states = sequelize.define(
'vorta_universe_states',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
profile: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
modules: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: [],
},
products: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: [],
},
ledger: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: [],
},
settings: {
type: DataTypes.JSONB,
allowNull: false,
defaultValue: {},
},
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
unique: true,
},
},
{
timestamps: true,
paranoid: true,
freezeTableName: true,
},
);
vorta_universe_states.associate = (db) => {
db.vorta_universe_states.belongsTo(db.users, {
as: 'user',
foreignKey: {
name: 'userId',
},
constraints: false,
});
db.vorta_universe_states.belongsTo(db.organizations, {
as: 'organizations',
foreignKey: {
name: 'organizationsId',
},
constraints: false,
});
db.vorta_universe_states.belongsTo(db.users, {
as: 'createdBy',
});
db.vorta_universe_states.belongsTo(db.users, {
as: 'updatedBy',
});
};
return vorta_universe_states;
};

View File

@ -6,7 +6,6 @@ const passport = require('passport');
const path = require('path');
const fs = require('fs');
const bodyParser = require('body-parser');
const db = require('./db/models');
const config = require('./config');
const swaggerUI = require('swagger-ui-express');
const swaggerJsDoc = require('swagger-jsdoc');
@ -20,6 +19,8 @@ const pexelsRoutes = require('./routes/pexels');
const organizationForAuthRoutes = require('./routes/organizationLogin');
const openaiRoutes = require('./routes/openai');
const vortaCommerceRoutes = require('./routes/vortaCommerce');
const vortaUniverseRoutes = require('./routes/vortaUniverse');
@ -245,6 +246,9 @@ app.use('/api/api_clients', passport.authenticate('jwt', {session: false}), api_
app.use('/api/audit_events', passport.authenticate('jwt', {session: false}), audit_eventsRoutes);
app.use('/api/vorta-commerce', passport.authenticate('jwt', {session: false}), vortaCommerceRoutes);
app.use('/api/vorta-universe', passport.authenticate('jwt', {session: false}), vortaUniverseRoutes);
app.use(
'/api/openai',
passport.authenticate('jwt', { session: false }),

View File

@ -0,0 +1,52 @@
const express = require('express');
const VortaCommerceService = require('../services/vortaCommerce');
const wrapAsync = require('../helpers').wrapAsync;
const router = express.Router();
router.get('/state', wrapAsync(async (req, res) => {
const payload = await VortaCommerceService.getState(req.currentUser);
res.status(200).send(payload);
}));
router.post('/orders/demo', wrapAsync(async (req, res) => {
const payload = await VortaCommerceService.createDemoOrder(req.currentUser);
res.status(200).send(payload);
}));
router.post('/inventory/restock', wrapAsync(async (req, res) => {
const payload = await VortaCommerceService.restockLowStock(req.currentUser);
res.status(200).send(payload);
}));
router.post('/payments/:paymentReference/capture', wrapAsync(async (req, res) => {
const payload = await VortaCommerceService.capturePayment(req.currentUser, req.params.paymentReference);
res.status(200).send(payload);
}));
router.post('/orders/:orderNumber/advance', wrapAsync(async (req, res) => {
const payload = await VortaCommerceService.advanceOrder(req.currentUser, req.params.orderNumber);
res.status(200).send(payload);
}));
router.post('/shipments/:trackingNumber/complete', wrapAsync(async (req, res) => {
const payload = await VortaCommerceService.completeShipment(req.currentUser, req.params.trackingNumber);
res.status(200).send(payload);
}));
router.post('/campaigns/:campaignId/launch', wrapAsync(async (req, res) => {
const payload = await VortaCommerceService.launchCampaign(req.currentUser, req.params.campaignId);
res.status(200).send(payload);
}));
router.post('/reports/close', wrapAsync(async (req, res) => {
const payload = await VortaCommerceService.closeDailyReport(req.currentUser);
res.status(200).send(payload);
}));
router.patch('/settings', wrapAsync(async (req, res) => {
const payload = await VortaCommerceService.updateSettings(req.currentUser, req.body.data || {});
res.status(200).send(payload);
}));
module.exports = router;

View File

@ -0,0 +1,71 @@
const express = require('express');
const VortaUniverseService = require('../services/vortaUniverse');
const wrapAsync = require('../helpers').wrapAsync;
const router = express.Router();
router.get('/state', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.getState(req.currentUser);
res.status(200).send(payload);
}));
router.post('/state/reset', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.resetState(req.currentUser);
res.status(200).send(payload);
}));
router.get('/social-feed', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.getSocialFeed(req.currentUser);
res.status(200).send(payload);
}));
router.post('/social-feed/reset', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.resetSocialFeed(req.currentUser);
res.status(200).send(payload);
}));
router.post('/posts', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.createSocialPost(req.currentUser, req.body.data || req.body || {});
res.status(200).send(payload);
}));
router.post('/posts/:postId/comments', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.createSocialComment(
req.currentUser,
req.params.postId,
req.body.data || req.body || {},
);
res.status(200).send(payload);
}));
router.post('/auth/otp/request', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.requestOtp(req.currentUser, req.body.data || req.body || {});
res.status(200).send(payload);
}));
router.post('/auth/otp/verify', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.verifyOtp(req.currentUser, req.body.data || req.body || {});
res.status(200).send(payload);
}));
router.patch('/trust-score', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.updateTrustScore(req.currentUser, req.body.data || req.body || {});
res.status(200).send(payload);
}));
router.post('/modules/:moduleId/run', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.runModule(req.currentUser, req.params.moduleId);
res.status(200).send(payload);
}));
router.post('/products', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.createProduct(req.currentUser, req.body.data || req.body || {});
res.status(200).send(payload);
}));
router.post('/products/demo', wrapAsync(async (req, res) => {
const payload = await VortaUniverseService.createDemoProduct(req.currentUser);
res.status(200).send(payload);
}));
module.exports = router;

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,763 @@
const bcrypt = require('bcrypt');
const db = require('../db/models');
const DEFAULT_PROFILE = {
businessName: 'VORTA Nexus Demo',
trustScore: 50,
verified: false,
walletBalance: 1850000,
geoseekRadiusKm: 7,
aiQuota: 32,
phone: '',
phoneVerified: false,
};
const DEFAULT_SETTINGS = {
trustAutoSync: true,
marketplaceMode: 'Demo Aktif',
riskReview: 'Medium',
};
const DEFAULT_MODULES = [
{
id: 'business-chat',
icon: '💬',
name: 'Chat Bisnis',
description: 'Inbox komersial untuk buyer, seller, partner, dan tim internal.',
metric: '128 percakapan',
status: 'Online',
trustImpact: 2,
launches: 0,
},
{
id: 'marketplace',
icon: '🛒',
name: 'Marketplace',
description: 'Etalase produk, seller score, katalog cepat, dan aktivitas listing.',
metric: '24 listing aktif',
status: 'Online',
trustImpact: 3,
launches: 0,
},
{
id: 'wallet',
icon: '💳',
name: 'Wallet',
description: 'Saldo, pembayaran, reward, dan settlement untuk pengguna VORTA.',
metric: 'Rp1,85 jt saldo',
status: 'Online',
trustImpact: 2,
launches: 0,
},
{
id: 'geoseek',
icon: '🌍',
name: 'GeoSeek',
description: 'Pencarian peluang, mitra, dan kebutuhan berdasarkan lokasi.',
metric: '7 km radius',
status: 'Beta',
trustImpact: 1,
launches: 0,
},
{
id: 'vorta-ai',
icon: '🤖',
name: 'VORTA AI',
description: 'Asisten AI untuk ide bisnis, analisis produk, dan rekomendasi aksi.',
metric: '32 prompt tersisa',
status: 'Siap',
trustImpact: 2,
launches: 0,
},
{
id: 'trust-system',
icon: '⭐',
name: 'Trust System',
description: 'Nexus Trust Score, validasi profil, reputasi transaksi, dan sinyal risiko.',
metric: 'Score dasar 50',
status: 'Aktif',
trustImpact: 4,
launches: 0,
},
];
const DEFAULT_PRODUCTS = [
{
id: 'VU-PRD-001',
name: 'VORTA Starter Kit',
price: 149000,
seller: 'Nexus Seller Lab',
category: 'Digital Toolkit',
stock: 120,
},
{
id: 'VU-PRD-002',
name: 'GeoSeek Local Leads',
price: 99000,
seller: 'VORTA Data Market',
category: 'Lead Pack',
stock: 45,
},
{
id: 'VU-PRD-003',
name: 'Trust Boost Verification',
price: 199000,
seller: 'VORTA Trust Desk',
category: 'Verification',
stock: 18,
},
];
const DEFAULT_LEDGER = [
{
id: 'VU-LOG-1',
title: 'Nexus Trust Score aktif',
detail: 'Baseline score dimulai dari 50 dan tersimpan di database PostgreSQL.',
time: 'Baru saja',
tone: 'success',
},
{
id: 'VU-LOG-2',
title: 'Marketplace demo tersambung',
detail: 'Produk awal siap diuji tanpa membuat aplikasi Express/SQLite terpisah.',
time: 'Baru saja',
tone: 'info',
},
];
const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
const cloneJson = (value) => JSON.parse(JSON.stringify(value));
const buildHttpError = (message, code = 400) => {
const error = new Error(message);
error.code = code;
return error;
};
const toNumber = (value, fallback = 0) => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
};
const getOrganizationId = (currentUser) => currentUser?.organizationId || currentUser?.organizationsId || null;
const makeImportHash = (scope, currentUser) => {
const userId = currentUser?.id || 'anonymous';
const organizationId = getOrganizationId(currentUser) || 'global';
return `vorta-universe-${organizationId}-${userId}-${scope}`;
};
const nowLabel = () => new Intl.DateTimeFormat('id-ID', {
dateStyle: 'medium',
timeStyle: 'short',
}).format(new Date());
const getDisplayName = (currentUser) => {
const firstName = currentUser?.firstName || '';
const lastName = currentUser?.lastName || '';
const fullName = `${firstName} ${lastName}`.trim();
return fullName || currentUser?.email || 'Founder VORTA';
};
const getScopedWhere = (currentUser) => ({
userId: currentUser?.id || null,
organizationsId: getOrganizationId(currentUser),
});
const normalizePhone = (phone) => String(phone || '').replace(/[^0-9+]/g, '').trim();
const makeOtp = () => String(Math.floor(100000 + Math.random() * 900000));
class VortaUniverseService {
static normalizeState(record) {
const profile = {
...cloneJson(DEFAULT_PROFILE),
...(record.profile || {}),
};
const modules = Array.isArray(record.modules) && record.modules.length
? record.modules
: cloneJson(DEFAULT_MODULES);
const products = Array.isArray(record.products) && record.products.length
? record.products
: cloneJson(DEFAULT_PRODUCTS);
const ledger = Array.isArray(record.ledger) && record.ledger.length
? record.ledger
: cloneJson(DEFAULT_LEDGER);
return {
profile: {
...profile,
trustScore: clamp(toNumber(profile.trustScore, DEFAULT_PROFILE.trustScore), 0, 100),
verified: Boolean(profile.verified || toNumber(profile.trustScore, 0) >= 80),
},
modules,
products,
ledger,
settings: {
...cloneJson(DEFAULT_SETTINGS),
...(record.settings || {}),
},
persistedAt: new Date().toISOString(),
};
}
static async ensureState(currentUser, transaction) {
const importHash = makeImportHash('state', currentUser);
const where = { importHash };
const existing = await db.vorta_universe_states.findOne({ where, transaction });
if (existing) {
return existing;
}
return db.vorta_universe_states.create(
{
userId: currentUser?.id || null,
organizationsId: getOrganizationId(currentUser),
profile: cloneJson(DEFAULT_PROFILE),
modules: cloneJson(DEFAULT_MODULES),
products: cloneJson(DEFAULT_PRODUCTS),
ledger: cloneJson(DEFAULT_LEDGER),
settings: cloneJson(DEFAULT_SETTINGS),
importHash,
createdById: currentUser?.id || null,
updatedById: currentUser?.id || null,
},
{ transaction },
);
}
static async getState(currentUser) {
const state = await db.sequelize.transaction(async (transaction) => this.ensureState(currentUser, transaction));
return this.normalizeState(state);
}
static async resetState(currentUser) {
await db.sequelize.transaction(async (transaction) => {
const state = await this.ensureState(currentUser, transaction);
await state.update(
{
profile: cloneJson(DEFAULT_PROFILE),
modules: cloneJson(DEFAULT_MODULES),
products: cloneJson(DEFAULT_PRODUCTS),
ledger: this.addLedgerItem(cloneJson(DEFAULT_LEDGER), {
title: 'State VORTA di-reset',
detail: 'Profile, module, produk, setting, dan ledger kembali ke data demo awal.',
tone: 'warning',
}),
settings: cloneJson(DEFAULT_SETTINGS),
updatedById: currentUser?.id || null,
},
{ transaction },
);
});
return this.getState(currentUser);
}
static addLedgerItem(ledger, item) {
return [
{
id: `VU-LOG-${Date.now()}`,
time: nowLabel(),
tone: 'info',
...item,
},
...(Array.isArray(ledger) ? ledger : []),
].slice(0, 12);
}
static async updateTrustScore(currentUser, payload) {
const requestedScore = toNumber(payload.score, Number.NaN);
if (!Number.isFinite(requestedScore)) {
throw buildHttpError('Score harus berupa angka 0 sampai 100.');
}
const nextScore = clamp(Math.round(requestedScore), 0, 100);
await db.sequelize.transaction(async (transaction) => {
const state = await this.ensureState(currentUser, transaction);
const current = this.normalizeState(state);
const previousScore = current.profile.trustScore;
await state.update(
{
profile: {
...current.profile,
trustScore: nextScore,
verified: nextScore >= 80,
},
ledger: this.addLedgerItem(current.ledger, {
title: 'Nexus Trust Score diperbarui',
detail: `Score berubah dari ${previousScore} ke ${nextScore}.`,
tone: nextScore >= previousScore ? 'success' : 'warning',
}),
updatedById: currentUser?.id || null,
},
{ transaction },
);
});
return this.getState(currentUser);
}
static async runModule(currentUser, moduleId) {
await db.sequelize.transaction(async (transaction) => {
const state = await this.ensureState(currentUser, transaction);
const current = this.normalizeState(state);
const target = current.modules.find((item) => item.id === moduleId);
if (!target) {
throw buildHttpError('Module VORTA Universe tidak ditemukan.', 404);
}
const modules = current.modules.map((item) => (
item.id === moduleId
? {
...item,
launches: toNumber(item.launches) + 1,
lastRun: new Date().toISOString(),
status: item.status === 'Beta' ? 'Beta Aktif' : 'Online',
}
: item
));
const nextScore = clamp(current.profile.trustScore + toNumber(target.trustImpact, 1), 0, 100);
await state.update(
{
profile: {
...current.profile,
trustScore: nextScore,
verified: nextScore >= 80,
},
modules,
ledger: this.addLedgerItem(current.ledger, {
title: `${target.name} dijalankan`,
detail: `Module ${target.name} aktif dan menambah ${toNumber(target.trustImpact, 1)} poin trust.`,
tone: 'success',
}),
updatedById: currentUser?.id || null,
},
{ transaction },
);
});
return this.getState(currentUser);
}
static buildProduct(payload, productNumber) {
const name = typeof payload.name === 'string' ? payload.name.trim() : '';
if (!name) {
throw buildHttpError('Nama produk wajib diisi.');
}
return {
id: payload.id || `VU-PRD-${String(productNumber).padStart(3, '0')}`,
name,
price: clamp(Math.round(toNumber(payload.price, 0)), 0, 999999999),
seller: payload.seller || 'VORTA Universe Seller',
category: payload.category || 'General',
stock: clamp(Math.round(toNumber(payload.stock, 1)), 0, 999999),
};
}
static async createProduct(currentUser, payload) {
await db.sequelize.transaction(async (transaction) => {
const state = await this.ensureState(currentUser, transaction);
const current = this.normalizeState(state);
const product = this.buildProduct(payload, current.products.length + 1);
await state.update(
{
products: [product, ...current.products].slice(0, 20),
ledger: this.addLedgerItem(current.ledger, {
title: 'Produk marketplace ditambahkan',
detail: `${product.name} dari ${product.seller} tersimpan di VORTA Universe.`,
tone: 'success',
}),
updatedById: currentUser?.id || null,
},
{ transaction },
);
});
return this.getState(currentUser);
}
static async addStateLedger(currentUser, item, transaction) {
const state = await this.ensureState(currentUser, transaction);
const current = this.normalizeState(state);
await state.update(
{
ledger: this.addLedgerItem(current.ledger, item),
updatedById: currentUser?.id || null,
},
{ transaction },
);
}
static serializeComment(comment) {
const row = comment.get ? comment.get({ plain: true }) : comment;
return {
id: row.id,
post_id: row.post_id,
user: row.user,
comment: row.comment,
parent_id: row.parent_id || '0',
created_at: row.created_at,
};
}
static serializePost(post, comments) {
const row = post.get ? post.get({ plain: true }) : post;
return {
id: row.id,
user: row.user,
content: row.content,
created_at: row.created_at,
comments,
};
}
static async ensureDefaultSocialFeed(currentUser, transaction) {
const where = getScopedWhere(currentUser);
const existingPost = await db.vorta_social_posts.findOne({ where, transaction });
if (existingPost) {
return;
}
const post = await db.vorta_social_posts.create(
{
...where,
user: 'VORTA System',
content: 'Selamat datang di feed VORTA Universe. Ini versi PostgreSQL dari prototype posts SQLite.',
importHash: makeImportHash('social-welcome-post', currentUser),
},
{ transaction },
);
await db.vorta_social_comments.create(
{
...where,
post_id: post.id,
user: getDisplayName(currentUser),
comment: 'Feed aktif: post, komentar, dan reply siap tersimpan permanen.',
parent_id: '0',
importHash: makeImportHash('social-welcome-comment', currentUser),
},
{ transaction },
);
}
static async buildSocialFeed(currentUser, transaction) {
const where = getScopedWhere(currentUser);
const posts = await db.vorta_social_posts.findAll({
where,
order: [['created_at', 'DESC']],
limit: 20,
transaction,
});
const postIds = posts.map((post) => post.id);
const comments = postIds.length
? await db.vorta_social_comments.findAll({
where: {
...where,
post_id: {
[db.Sequelize.Op.in]: postIds,
},
},
order: [['created_at', 'ASC']],
transaction,
})
: [];
const commentsByPost = comments.reduce((acc, comment) => {
const serialized = this.serializeComment(comment);
acc[serialized.post_id] = acc[serialized.post_id] || [];
acc[serialized.post_id].push(serialized);
return acc;
}, {});
return {
posts: posts.map((post) => this.serializePost(post, commentsByPost[post.id] || [])),
stats: {
postsCount: posts.length,
commentsCount: comments.length,
},
syncedAt: new Date().toISOString(),
};
}
static async getSocialFeed(currentUser) {
return db.sequelize.transaction(async (transaction) => {
await this.ensureDefaultSocialFeed(currentUser, transaction);
return this.buildSocialFeed(currentUser, transaction);
});
}
static async resetSocialFeed(currentUser) {
await db.sequelize.transaction(async (transaction) => {
const where = getScopedWhere(currentUser);
await db.vorta_social_comments.destroy({ where, transaction });
await db.vorta_social_posts.destroy({ where, transaction });
await this.ensureDefaultSocialFeed(currentUser, transaction);
await this.addStateLedger(currentUser, {
title: 'Social feed VORTA di-reset',
detail: 'Posts dan comments scoped user ini dikembalikan ke feed demo awal.',
tone: 'warning',
}, transaction);
});
return {
feed: await this.getSocialFeed(currentUser),
state: await this.getState(currentUser),
};
}
static async createSocialPost(currentUser, payload) {
const content = typeof payload.content === 'string' ? payload.content.trim() : '';
if (!content) {
throw buildHttpError('Konten post wajib diisi.');
}
await db.sequelize.transaction(async (transaction) => {
await this.ensureDefaultSocialFeed(currentUser, transaction);
const where = getScopedWhere(currentUser);
await db.vorta_social_posts.create(
{
...where,
user: payload.user || getDisplayName(currentUser),
content,
},
{ transaction },
);
await this.addStateLedger(currentUser, {
title: 'Post komunitas dibuat',
detail: 'Konten baru tersimpan di tabel vorta_social_posts PostgreSQL.',
tone: 'success',
}, transaction);
});
return {
feed: await this.getSocialFeed(currentUser),
state: await this.getState(currentUser),
};
}
static async createSocialComment(currentUser, postId, payload) {
const commentText = typeof payload.comment === 'string' ? payload.comment.trim() : '';
if (!commentText) {
throw buildHttpError('Komentar wajib diisi.');
}
await db.sequelize.transaction(async (transaction) => {
const where = getScopedWhere(currentUser);
const post = await db.vorta_social_posts.findOne({
where: {
...where,
id: postId,
},
transaction,
});
if (!post) {
throw buildHttpError('Post VORTA tidak ditemukan.', 404);
}
await db.vorta_social_comments.create(
{
...where,
post_id: post.id,
user: payload.user || getDisplayName(currentUser),
comment: commentText,
parent_id: payload.parent_id || '0',
},
{ transaction },
);
await this.addStateLedger(currentUser, {
title: 'Komentar komunitas dibuat',
detail: 'Komentar tersimpan di tabel vorta_social_comments PostgreSQL.',
tone: 'info',
}, transaction);
});
return {
feed: await this.getSocialFeed(currentUser),
state: await this.getState(currentUser),
};
}
static async requestOtp(currentUser, payload) {
const phone = normalizePhone(payload.phone);
if (!phone || phone.length < 8) {
throw buildHttpError('Nomor HP wajib diisi minimal 8 digit.');
}
const otp = makeOtp();
const otpHash = await bcrypt.hash(otp, 10);
const expiresAt = new Date(Date.now() + 5 * 60 * 1000);
await db.sequelize.transaction(async (transaction) => {
const where = getScopedWhere(currentUser);
await db.vorta_login_otps.update(
{
status: 'superseded',
updatedById: currentUser?.id || null,
},
{
where: {
...where,
phone,
status: 'pending',
},
transaction,
},
);
await db.vorta_login_otps.create(
{
...where,
phone,
otpHash,
expiresAt,
importHash: `vorta-otp-${Date.now()}-${Math.random().toString(16).slice(2)}`,
createdById: currentUser?.id || null,
updatedById: currentUser?.id || null,
},
{ transaction },
);
await this.addStateLedger(currentUser, {
title: 'OTP development dibuat',
detail: `Kode OTP untuk ${phone} berlaku 5 menit dan tersimpan sebagai hash.`,
tone: 'info',
}, transaction);
});
return {
message: 'OTP dev berhasil dibuat. Untuk produksi, sambungkan endpoint ini ke SMS/WhatsApp provider.',
phone,
devOtp: otp,
expiresAt,
state: await this.getState(currentUser),
};
}
static async verifyOtp(currentUser, payload) {
const phone = normalizePhone(payload.phone);
const otp = String(payload.otp || '').trim();
if (!phone || !otp) {
throw buildHttpError('Nomor HP dan OTP wajib diisi.');
}
await db.sequelize.transaction(async (transaction) => {
const where = getScopedWhere(currentUser);
const record = await db.vorta_login_otps.findOne({
where: {
...where,
phone,
status: 'pending',
expiresAt: {
[db.Sequelize.Op.gt]: new Date(),
},
},
order: [['createdAt', 'DESC']],
transaction,
});
if (!record) {
throw buildHttpError('OTP tidak ditemukan atau sudah kedaluwarsa.', 400);
}
const isValid = await bcrypt.compare(otp, record.otpHash);
const attempts = toNumber(record.attempts, 0) + 1;
if (!isValid) {
await record.update(
{
attempts,
status: attempts >= 5 ? 'locked' : 'pending',
updatedById: currentUser?.id || null,
},
{ transaction },
);
throw buildHttpError('OTP salah. Silakan cek kode dan coba lagi.', 400);
}
await record.update(
{
attempts,
status: 'verified',
verifiedAt: new Date(),
updatedById: currentUser?.id || null,
},
{ transaction },
);
const state = await this.ensureState(currentUser, transaction);
const current = this.normalizeState(state);
const nextScore = clamp(current.profile.trustScore + 5, 0, 100);
await state.update(
{
profile: {
...current.profile,
phone,
phoneVerified: true,
trustScore: nextScore,
verified: nextScore >= 80,
},
ledger: this.addLedgerItem(current.ledger, {
title: 'OTP HP terverifikasi',
detail: `${phone} lolos verifikasi OTP dan menambah 5 poin trust.`,
tone: 'success',
}),
updatedById: currentUser?.id || null,
},
{ transaction },
);
});
return {
message: 'OTP valid. Nomor HP terverifikasi dan Trust Score naik +5.',
verified: true,
state: await this.getState(currentUser),
};
}
static async createDemoProduct(currentUser) {
const productIndex = Date.now().toString().slice(-4);
return this.createProduct(currentUser, {
id: `VU-PRD-${productIndex}`,
name: `Produk Demo Universe ${productIndex}`,
price: 125000 + (Number(productIndex) % 5) * 25000,
seller: 'VORTA Demo Seller',
category: 'Demo Product',
stock: 24 + (Number(productIndex) % 20),
});
}
}
module.exports = VortaUniverseService;

View File

@ -1,7 +1,66 @@
import * as icon from '@mdi/js';
import { MenuAsideItem } from './interfaces';
const resolveMenuIcon = (name: string) => (
name in icon ? (icon[name as keyof typeof icon] as string) : icon.mdiViewDashboardOutline
);
const menuAside: MenuAsideItem[] = [
{
icon: resolveMenuIcon('mdiOrbitVariant'),
label: 'VORTA Universe',
menu: [
{
href: '/vorta-universe#vorta-overview',
icon: resolveMenuIcon('mdiViewDashboardOutline'),
label: 'Dashboard',
},
{
href: '/vorta-universe#vorta-auth',
icon: resolveMenuIcon('mdiShieldKeyOutline'),
label: 'Login & OTP',
},
{
href: '/vorta-universe#vorta-modules',
icon: resolveMenuIcon('mdiApps'),
label: 'Modules',
},
{
href: '/vorta-universe#vorta-social',
icon: resolveMenuIcon('mdiPostOutline'),
label: 'Feed Bisnis',
},
{
href: '/vorta-universe#vorta-marketplace',
icon: resolveMenuIcon('mdiStorefrontOutline'),
label: 'Marketplace',
},
{
href: '/vorta-universe#vorta-settings',
icon: resolveMenuIcon('mdiCogOutline'),
label: 'Settings',
},
{
href: '/trust_profiles/trust_profiles-list',
icon: resolveMenuIcon('mdiShieldCheck'),
label: 'Nexus Score Entity',
permissions: 'READ_TRUST_PROFILES',
},
{
href: '/wallets/wallets-list',
icon: resolveMenuIcon('mdiWallet'),
label: 'Wallet Entity',
permissions: 'READ_WALLETS',
},
{
href: '/products/products-list',
icon: resolveMenuIcon('mdiPackageVariantClosed'),
label: 'Products Entity',
permissions: 'READ_PRODUCTS',
},
],
},
{
href: '/vorta-commerce',
icon:

View File

@ -32,6 +32,25 @@ type TrustReport = TrustForm & {
recommendations: string[];
};
type SectionLink = {
id: string;
label: string;
};
type WorkspaceModule = {
title: string;
description: string;
metric: string;
sectionId: string;
};
type Pillar = {
title: string;
description: string;
route: string;
actionLabel: string;
};
const defaultForm: TrustForm = {
profileName: '',
profileType: 'umkm',
@ -52,6 +71,72 @@ const profileTypeLabels: Record<TrustForm['profileType'], string> = {
company: 'Perusahaan',
};
const sectionLinks: SectionLink[] = [
{ id: 'home', label: 'Beranda' },
{ id: 'workspace-preview', label: 'Workspace' },
{ id: 'pillars', label: 'Super-App' },
{ id: 'trust-flow', label: 'Skor' },
{ id: 'reports', label: 'Laporan' },
];
const workspaceModules: WorkspaceModule[] = [
{
title: 'Chat bisnis',
description: 'Buka alur percakapan tim, pembeli, dan penjual.',
metric: '42 pesan',
sectionId: 'pillars',
},
{
title: 'Feed sosial',
description: 'Lihat komunitas dan reputasi yang terhubung skor Nexus.',
metric: '31 update',
sectionId: 'trust-flow',
},
{
title: 'Marketplace',
description: 'Jalankan simulasi toko, produk, dan ulasan terverifikasi.',
metric: '18 produk',
sectionId: 'trust-flow',
},
{
title: 'Dompet digital',
description: 'Pantau dompet, transaksi, dan aktivitas pembayaran ringan.',
metric: '37 transaksi',
sectionId: 'reports',
},
];
const pillars: Pillar[] = [
{
title: 'Mega Super-App',
description:
'Chat bisnis, sosial, marketplace, dan dompet dalam satu ruang kerja ringan.',
route: '/mega-super-app',
actionLabel: 'Buka Super-App',
},
{
title: 'Vorta Nexus',
description:
'Skor kepercayaan 0-100 untuk identitas, bisnis, ulasan, dan akses komunitas.',
route: '/vorta-universe',
actionLabel: 'Buka Nexus',
},
{
title: 'Facta.AI',
description:
'Riset bebas SEO spam dengan Truth Score dan peta rujukan langsung.',
route: '/vorta-commerce',
actionLabel: 'Buka Commerce',
},
{
title: 'Vorta Synapse',
description:
'Protokol distribusi, afiliasi, dan logistik pintar untuk ekonomi kreator.',
route: '/vorta-synapse',
actionLabel: 'Buka Synapse',
},
];
const getTier = (score: number) => {
if (score >= 86) return 'Nexus Prime';
if (score >= 70) return 'Pertumbuhan Terverifikasi';
@ -150,6 +235,13 @@ export default function VortaLanding() {
const [reports, setReports] = useState<TrustReport[]>([]);
const [activeReportId, setActiveReportId] = useState<string>('');
const [errorMessage, setErrorMessage] = useState('');
const [activeWorkspaceModule, setActiveWorkspaceModule] = useState(
workspaceModules[0].title,
);
const [frontActionMessage, setFrontActionMessage] = useState(
'Semua menu dan tombol muka siap dijalankan.',
);
const [activePillarTitle, setActivePillarTitle] = useState(pillars[0].title);
useEffect(() => {
try {
@ -208,6 +300,9 @@ export default function VortaLanding() {
}
setErrorMessage('');
setFrontActionMessage(
`Laporan Kepercayaan untuk ${trimmedName} berhasil dibuat.`,
);
saveReport({ ...form, profileName: trimmedName });
};
@ -226,40 +321,67 @@ export default function VortaLanding() {
setForm(sampleForm);
setErrorMessage('');
setFrontActionMessage('Contoh cepat dibuat dan detail laporan dibuka.');
saveReport(sampleForm);
};
const handleResetForm = () => {
setForm(defaultForm);
setErrorMessage('');
setFrontActionMessage('Formulir simulator dikembalikan ke nilai awal.');
scrollToSection('trust-flow');
};
const handleSectionNavigation = (sectionId: string, label: string) => {
setFrontActionMessage(`Menu ${label} dibuka.`);
scrollToSection(sectionId);
};
const handleWorkspaceModule = (module: WorkspaceModule) => {
setActiveWorkspaceModule(module.title);
setFrontActionMessage(`${module.title} aktif: ${module.description}`);
scrollToSection(module.sectionId);
};
const handlePillarPreview = (pillar: Pillar) => {
setActivePillarTitle(pillar.title);
setFrontActionMessage(
`${pillar.title} dipilih. Tekan "${pillar.actionLabel}" untuk membuka halaman modul.`,
);
scrollToSection('pillars');
};
const handleStartDemo = () => {
setForm({
...defaultForm,
profileName: 'Demo VORTA Universe',
verifiedDocuments: '3',
citedSources: '3',
hasTaxId: true,
verifiedReviews: true,
});
setErrorMessage('');
setFrontActionMessage(
'Demo skor siap. Periksa formulir lalu tekan Buat Laporan Kepercayaan.',
);
scrollToSection('trust-flow');
};
const handleClearReports = () => {
setReports([]);
setActiveReportId('');
setFrontActionMessage(
'Semua riwayat laporan demo di browser ini dibersihkan.',
);
scrollToSection('reports');
};
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 (
<>
<Head>
@ -270,7 +392,10 @@ export default function VortaLanding() {
/>
</Head>
<main className='min-h-screen overflow-hidden bg-[#07111f] text-white'>
<main
id='home'
className='min-h-screen overflow-hidden bg-[#07111f] text-white'
>
<section className='relative isolate px-5 py-5 sm:px-8 lg:px-12'>
<div className='absolute inset-0 -z-10 bg-[radial-gradient(circle_at_top_left,#1de9b6_0,transparent_30%),radial-gradient(circle_at_80%_20%,#6c63ff_0,transparent_28%),linear-gradient(135deg,#07111f_0%,#101b34_45%,#07111f_100%)] opacity-95' />
<div className='absolute left-1/2 top-20 -z-10 h-72 w-72 -translate-x-1/2 rounded-full bg-cyan-400/20 blur-3xl' />
@ -286,18 +411,14 @@ export default function VortaLanding() {
<span>VORTA UNIVERSE</span>
</Link>
<div className='hidden items-center gap-2 text-sm text-slate-300 md:flex'>
{[
['pillars', 'Super-App'],
['trust-flow', 'Skor'],
['reports', 'Laporan'],
].map(([sectionId, label]) => (
{sectionLinks.map((item) => (
<button
key={sectionId}
key={item.id}
type='button'
onClick={() => scrollToSection(sectionId)}
onClick={() => handleSectionNavigation(item.id, item.label)}
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}
{item.label}
</button>
))}
</div>
@ -311,18 +432,14 @@ export default function VortaLanding() {
</nav>
<div className='mx-auto mt-4 flex max-w-7xl flex-wrap justify-center gap-2 text-sm text-slate-200 md:hidden'>
{[
['pillars', 'Super-App'],
['trust-flow', 'Skor'],
['reports', 'Laporan'],
].map(([sectionId, label]) => (
{sectionLinks.map((item) => (
<button
key={sectionId}
key={item.id}
type='button'
onClick={() => scrollToSection(sectionId)}
onClick={() => handleSectionNavigation(item.id, item.label)}
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}
{item.label}
</button>
))}
</div>
@ -343,15 +460,17 @@ export default function VortaLanding() {
</p>
<div className='mt-9 flex flex-col gap-3 sm:flex-row'>
<BaseButton
onClick={() => scrollToSection('pillars')}
onClick={() =>
handleSectionNavigation('pillars', 'Alur Super-App')
}
label='Lihat Alur Super-App'
color='success'
roundedFull
className='border-emerald-300 bg-[#1DE9B6] px-7 py-3 text-slate-950 hover:bg-white'
/>
<BaseButton
onClick={() => scrollToSection('trust-flow')}
label='Simulasikan Kepercayaan'
onClick={handleStartDemo}
label='Jalankan Demo Skor'
color='white'
outline
roundedFull
@ -366,6 +485,9 @@ export default function VortaLanding() {
className='border-white/30 bg-white/5 px-7 py-3 text-white hover:bg-white hover:text-slate-950'
/>
</div>
<div className='mt-5 rounded-3xl border border-white/10 bg-white/[0.08] px-5 py-4 text-sm font-semibold text-cyan-100'>
Status: {frontActionMessage}
</div>
<div className='mt-10 grid max-w-xl grid-cols-3 gap-3'>
{stats.map((stat) => (
<div
@ -383,7 +505,7 @@ export default function VortaLanding() {
</div>
</div>
<div className='relative'>
<div id='workspace-preview' className='relative scroll-mt-24'>
<div className='absolute -inset-4 rounded-[2rem] bg-gradient-to-br from-cyan-300/30 via-indigo-400/20 to-emerald-300/20 blur-2xl' />
<CardBox
className='relative border-white/10 bg-white/10 text-white shadow-2xl shadow-black/30 backdrop-blur-2xl'
@ -406,32 +528,48 @@ export default function VortaLanding() {
</div>
<div className='space-y-5 p-6'>
<div className='grid grid-cols-2 gap-3'>
{[
'Chat bisnis',
'Feed sosial',
'Marketplace',
'Dompet digital',
].map((item) => (
<div
key={item}
className='rounded-2xl border border-white/10 bg-white/[0.08] p-4'
{workspaceModules.map((module) => (
<button
key={module.title}
type='button'
onClick={() => handleWorkspaceModule(module)}
className={`rounded-2xl border p-4 text-left transition hover:-translate-y-0.5 hover:border-cyan-200/40 hover:bg-white/15 focus:outline-none focus:ring-4 focus:ring-cyan-300/30 ${
activeWorkspaceModule === module.title
? 'border-cyan-200/60 bg-cyan-200/15'
: 'border-white/10 bg-white/[0.08]'
}`}
>
<div className='mb-3 h-2 w-12 rounded-full bg-gradient-to-r from-[#1DE9B6] to-[#6C63FF]' />
<p className='text-sm font-semibold'>{item}</p>
</div>
<p className='text-sm font-semibold'>{module.title}</p>
<p className='mt-2 text-xs leading-5 text-slate-300'>
{module.description}
</p>
<span className='mt-3 inline-flex rounded-full bg-white/10 px-3 py-1 text-[11px] font-bold uppercase tracking-[0.16em] text-cyan-100'>
{module.metric}
</span>
</button>
))}
</div>
<div className='rounded-3xl border border-cyan-200/20 bg-cyan-200/10 p-5'>
<div className='flex items-end justify-between'>
<div>
<p className='text-sm text-cyan-100/80'>
Aktivitas terpadu hari ini
Modul aktif: {activeWorkspaceModule}
</p>
<p className='text-5xl font-black text-cyan-100'>128</p>
</div>
<p className='rounded-full bg-white px-3 py-1 text-sm font-bold text-slate-950'>
Transaksi & pesan sinkron
</p>
<button
type='button'
onClick={() =>
handleSectionNavigation(
'trust-flow',
'Simulator Skor',
)
}
className='rounded-full bg-white px-3 py-1 text-sm font-bold text-slate-950 transition hover:bg-cyan-100 focus:outline-none focus:ring-4 focus:ring-cyan-300/30'
>
Jalankan sinkronisasi
</button>
</div>
<div className='mt-5 h-3 rounded-full bg-slate-900/70'>
<div className='h-3 w-[92%] rounded-full bg-gradient-to-r from-[#1DE9B6] to-[#6C63FF]' />
@ -456,24 +594,57 @@ export default function VortaLanding() {
Satu ruang kerja ringan yang menghubungkan percakapan,
komunitas, perdagangan, pembayaran, dan reputasi pengguna.
</h2>
<p className='mt-4 rounded-3xl border border-white/10 bg-white/[0.08] px-5 py-3 text-sm font-semibold text-cyan-100'>
Pilar aktif: {activePillarTitle}
</p>
</div>
<div className='grid gap-4 md:grid-cols-2 xl:grid-cols-4'>
{pillars.map(([title, description], index) => (
{pillars.map((pillar, index) => (
<article
key={title}
key={pillar.title}
className={`group rounded-[1.75rem] border p-6 transition hover:-translate-y-1 hover:border-cyan-200/40 hover:bg-white/10 ${
index === 0
activePillarTitle === pillar.title
? 'border-cyan-200/50 bg-cyan-200/10 shadow-2xl shadow-cyan-950/30'
: 'border-white/10 bg-slate-950/40'
}`}
>
<div className='mb-8 flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-[#1DE9B6] to-[#6C63FF] text-lg font-black text-slate-950'>
0{index + 1}
<button
type='button'
onClick={() => handlePillarPreview(pillar)}
className='block w-full text-left focus:outline-none focus:ring-4 focus:ring-cyan-300/30'
>
<div className='mb-8 flex h-12 w-12 items-center justify-center rounded-2xl bg-gradient-to-br from-[#1DE9B6] to-[#6C63FF] text-lg font-black text-slate-950'>
0{index + 1}
</div>
<h3 className='text-xl font-bold'>{pillar.title}</h3>
<p className='mt-3 text-sm leading-6 text-slate-300'>
{pillar.description}
</p>
</button>
<div className='mt-6 flex flex-col gap-2'>
<BaseButton
href={pillar.route}
label={pillar.actionLabel}
color={index === 0 ? 'success' : 'white'}
outline={index !== 0}
roundedFull
small
className={
index === 0
? 'border-emerald-300 bg-[#1DE9B6] text-slate-950 hover:bg-white'
: 'border-white/20 bg-white/5 text-white hover:bg-white hover:text-slate-950'
}
/>
<BaseButton
onClick={() => handlePillarPreview(pillar)}
label='Preview di halaman ini'
color='white'
outline
roundedFull
small
className='border-white/20 bg-transparent text-slate-200 hover:bg-white/10'
/>
</div>
<h3 className='text-xl font-bold'>{title}</h3>
<p className='mt-3 text-sm leading-6 text-slate-300'>
{description}
</p>
</article>
))}
</div>
@ -490,9 +661,9 @@ export default function VortaLanding() {
Skor Kepercayaan untuk Super-App
</h2>
<p className='mt-4 text-slate-300'>
Setiap akun, toko, percakapan, ulasan, dan transaksi bisa
diberi sinyal kepercayaan agar marketplace dan komunitas tetap
aman tanpa terasa berat bagi pengguna.
Setiap akun, toko, percakapan, ulasan, dan transaksi bisa diberi
sinyal kepercayaan agar marketplace dan komunitas tetap aman
tanpa terasa berat bagi pengguna.
</p>
<div className='mt-6 rounded-3xl border border-white/10 bg-white/[0.08] p-5 text-sm leading-6 text-slate-300'>
<strong className='text-white'>Aturan validasi:</strong> nama
@ -656,6 +827,26 @@ export default function VortaLanding() {
<p className='mt-2 text-sm text-slate-300'>
Pilih laporan untuk membuka detail simulasi.
</p>
<div className='mt-4 flex flex-wrap gap-2'>
<BaseButton
onClick={handleSampleReport}
label='Jalankan Contoh'
color='white'
outline
roundedFull
small
className='border-white/20 bg-white/5 text-white hover:bg-white hover:text-slate-950'
/>
<BaseButton
onClick={handleClearReports}
label='Bersihkan Laporan'
color='danger'
outline
roundedFull
small
className='border-rose-200/40 bg-rose-300/10 text-rose-100 hover:bg-rose-200 hover:text-slate-950'
/>
</div>
</div>
<div className='space-y-3 p-4'>
{reports.length ? (
@ -792,6 +983,13 @@ export default function VortaLanding() {
Buat Laporan Kepercayaan pertama untuk melihat tier,
progress bar, dan rekomendasi.
</p>
<BaseButton
onClick={handleSampleReport}
label='Buat Contoh Sekarang'
color='info'
roundedFull
className='mt-6 bg-slate-950 text-white hover:bg-cyan-600'
/>
</div>
)}
</CardBox>

View File

@ -1,6 +1,7 @@
import * as icon from '@mdi/js';
import axios from 'axios';
import Head from 'next/head';
import React, { ReactElement, useMemo, useState } from 'react';
import React, { ReactElement, useCallback, useEffect, useMemo, useState } from 'react';
import BaseButton from '../components/BaseButton';
import BaseIcon from '../components/BaseIcon';
import CardBox from '../components/CardBox';
@ -107,6 +108,20 @@ type Settings = {
paymentAutoCapture: boolean;
};
type CommerceState = {
products: Product[];
customers: Customer[];
orders: Order[];
payments: Payment[];
shipments: Shipment[];
campaigns: Campaign[];
ledger: LedgerItem[];
settings: Settings;
persistedAt?: string;
};
type CommerceAction = () => Promise<{ data: CommerceState }>;
const initialProducts: Product[] = [
{
id: 'SKU-EL-001',
@ -416,6 +431,56 @@ const VortaCommercePage = () => {
const [campaigns, setCampaigns] = useState<Campaign[]>(initialCampaigns);
const [ledger, setLedger] = useState<LedgerItem[]>(initialLedger);
const [settings, setSettings] = useState<Settings>(initialSettings);
const [isLoadingCommerce, setIsLoadingCommerce] = useState(true);
const [isSavingCommerce, setIsSavingCommerce] = useState(false);
const [commerceError, setCommerceError] = useState<string | null>(null);
const [persistedAt, setPersistedAt] = useState<string | null>(null);
const applyCommerceState = useCallback((payload: CommerceState) => {
setProducts(payload.products || initialProducts);
setCustomers(payload.customers || initialCustomers);
setOrders(payload.orders || initialOrders);
setPayments(payload.payments || initialPayments);
setShipments(payload.shipments || initialShipments);
setCampaigns(payload.campaigns || initialCampaigns);
setLedger(payload.ledger || initialLedger);
setSettings(payload.settings || initialSettings);
setPersistedAt(payload.persistedAt || new Date().toISOString());
}, []);
const loadCommerceState = useCallback(async () => {
setIsLoadingCommerce(true);
setCommerceError(null);
try {
const response = await axios.get<CommerceState>('/vorta-commerce/state');
applyCommerceState(response.data);
} catch (error) {
console.error('Failed to load VORTA-COMMERCE state:', error);
setCommerceError('Data database belum bisa dimuat. Fallback demo lokal tetap ditampilkan.');
} finally {
setIsLoadingCommerce(false);
}
}, [applyCommerceState]);
useEffect(() => {
loadCommerceState();
}, [loadCommerceState]);
const runCommerceAction = useCallback(async (action: CommerceAction, errorMessage: string) => {
setIsSavingCommerce(true);
setCommerceError(null);
try {
const response = await action();
applyCommerceState(response.data);
} catch (error) {
console.error(errorMessage, error);
setCommerceError(`${errorMessage} Cek backend log untuk detail.`);
} finally {
setIsSavingCommerce(false);
}
}, [applyCommerceState]);
const metrics = useMemo(() => {
const paidRevenue = payments
@ -459,223 +524,75 @@ const VortaCommercePage = () => {
[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',
runCommerceAction(
() => axios.post<CommerceState>('/vorta-commerce/inventory/restock'),
'Gagal menjalankan restock cepat.',
);
};
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,
),
runCommerceAction(
() => axios.post<CommerceState>('/vorta-commerce/orders/demo'),
'Gagal membuat order demo.',
);
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)),
runCommerceAction(
() => axios.post<CommerceState>(`/vorta-commerce/payments/${paymentId}/capture`),
'Gagal capture payment.',
);
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<OrderStatus, OrderStatus> = {
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,
),
runCommerceAction(
() => axios.post<CommerceState>(`/vorta-commerce/orders/${orderId}/advance`),
'Gagal memajukan status order.',
);
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)),
runCommerceAction(
() => axios.post<CommerceState>(`/vorta-commerce/shipments/${shipmentId}/complete`),
'Gagal menandai shipment terkirim.',
);
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,
),
runCommerceAction(
() => axios.post<CommerceState>(`/vorta-commerce/campaigns/${campaignId}/launch`),
'Gagal launch campaign.',
);
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',
runCommerceAction(
() => axios.post<CommerceState>('/vorta-commerce/reports/close'),
'Gagal mengunci laporan harian.',
);
};
const toggleFulfillmentMode = () => {
setSettings((current) => ({
...current,
fulfillmentMode: current.fulfillmentMode === 'Otomatis' ? 'Manual Review' : 'Otomatis',
}));
pushLedger('Pengaturan fulfillment berubah', 'Mode fulfillment toko diperbarui.', 'info');
runCommerceAction(
() => axios.patch<CommerceState>('/vorta-commerce/settings', {
data: {
fulfillmentMode: settings.fulfillmentMode === 'Otomatis' ? 'Manual Review' : 'Otomatis',
},
}),
'Gagal mengubah fulfillment mode.',
);
};
const toggleBooleanSetting = (key: 'lowStockAlert' | 'paymentAutoCapture') => {
setSettings((current) => ({ ...current, [key]: !current[key] }));
runCommerceAction(
() => axios.patch<CommerceState>('/vorta-commerce/settings', {
data: {
[key]: !settings[key],
},
}),
'Gagal menyimpan automation setting.',
);
};
const renderMetricCard = (label: string, value: string, helper: string, iconName: string, accent: string) => (
@ -972,7 +889,7 @@ const VortaCommercePage = () => {
icon={resolveIcon('mdiArrowRightCircleOutline')}
color={order.status === 'Selesai' ? 'success' : 'info'}
small
disabled={order.status === 'Selesai'}
disabled={order.status === 'Selesai' || isSavingCommerce}
onClick={() => advanceOrder(order.id)}
/>
</div>
@ -1040,7 +957,7 @@ const VortaCommercePage = () => {
icon={resolveIcon('mdiCheckCircleOutline')}
color={payment.status === 'Berhasil' ? 'success' : 'info'}
small
disabled={payment.status === 'Berhasil'}
disabled={payment.status === 'Berhasil' || isSavingCommerce}
onClick={() => capturePayment(payment.id)}
/>
</div>
@ -1075,7 +992,7 @@ const VortaCommercePage = () => {
icon={resolveIcon('mdiTruckCheckOutline')}
color={shipment.status === 'Terkirim' ? 'success' : 'info'}
small
disabled={shipment.status === 'Terkirim'}
disabled={shipment.status === 'Terkirim' || isSavingCommerce}
onClick={() => completeShipment(shipment.id)}
/>
</div>
@ -1141,6 +1058,7 @@ const VortaCommercePage = () => {
icon={resolveIcon('mdiFileChartOutline')}
color="info"
small
disabled={isSavingCommerce}
onClick={() => closeDailyReport()}
/>
</div>
@ -1214,6 +1132,7 @@ const VortaCommercePage = () => {
icon={resolveIcon('mdiSwapHorizontal')}
color="info"
small
disabled={isSavingCommerce}
onClick={() => toggleFulfillmentMode()}
/>
</div>
@ -1233,6 +1152,7 @@ const VortaCommercePage = () => {
label={settings.lowStockAlert ? 'Aktif' : 'Nonaktif'}
color={settings.lowStockAlert ? 'success' : 'warning'}
small
disabled={isSavingCommerce}
onClick={() => toggleBooleanSetting('lowStockAlert')}
/>
</div>
@ -1245,6 +1165,7 @@ const VortaCommercePage = () => {
label={settings.paymentAutoCapture ? 'Aktif' : 'Nonaktif'}
color={settings.paymentAutoCapture ? 'success' : 'warning'}
small
disabled={isSavingCommerce}
onClick={() => toggleBooleanSetting('paymentAutoCapture')}
/>
</div>
@ -1279,14 +1200,26 @@ const VortaCommercePage = () => {
main
>
<BaseButton
label="Aplikasi Utama"
icon={resolveIcon('mdiStarFourPointsOutline')}
label={isSavingCommerce ? 'Menyimpan...' : isLoadingCommerce ? 'Memuat DB...' : 'Database Aktif'}
icon={resolveIcon('mdiDatabaseCheckOutline')}
color="success"
small
disabled
/>
</SectionTitleLineWithButton>
{commerceError ? (
<div className="mb-4 rounded-2xl border border-red-200 bg-red-50 p-4 text-sm font-medium text-red-700 dark:border-red-900/40 dark:bg-red-900/20 dark:text-red-200">
{commerceError}
</div>
) : null}
{(isLoadingCommerce || isSavingCommerce) ? (
<div className="mb-4 rounded-2xl border border-blue-200 bg-blue-50 p-4 text-sm font-medium text-blue-700 dark:border-blue-900/40 dark:bg-blue-900/20 dark:text-blue-200">
{isLoadingCommerce ? 'Memuat data VORTA-COMMERCE dari database...' : 'Menyimpan perubahan ke database...'}
</div>
) : null}
<div className="mb-6 overflow-hidden rounded-3xl bg-gradient-to-r from-slate-950 via-blue-950 to-emerald-900 p-6 text-white shadow-xl">
<div className="grid grid-cols-1 gap-6 xl:grid-cols-3">
<div className="xl:col-span-2">
@ -1300,12 +1233,14 @@ const VortaCommercePage = () => {
label="Buat Order Demo"
icon={resolveIcon('mdiCartPlus')}
color="success"
disabled={isSavingCommerce}
onClick={() => createFlashOrder()}
/>
<BaseButton
label="Restock Cepat"
icon={resolveIcon('mdiPackageVariantPlus')}
color="info"
disabled={isSavingCommerce}
onClick={() => restockLowStock()}
/>
<BaseButton
@ -1326,6 +1261,7 @@ const VortaCommercePage = () => {
<div className="flex justify-between"><span>Mode</span><strong>{settings.fulfillmentMode}</strong></div>
<div className="flex justify-between"><span>Open orders</span><strong>{metrics.openOrders}</strong></div>
<div className="flex justify-between"><span>Low stock</span><strong>{metrics.lowStockCount}</strong></div>
<div className="flex justify-between"><span>Sync</span><strong>{persistedAt ? 'Persisten' : 'Lokal'}</strong></div>
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff