Autosave: 20260619-030652
This commit is contained in:
parent
0925ce59ce
commit
f954e3c153
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
93
backend/src/db/models/vorta_commerce_states.js
Normal file
93
backend/src/db/models/vorta_commerce_states.js
Normal 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;
|
||||
};
|
||||
75
backend/src/db/models/vorta_login_otps.js
Normal file
75
backend/src/db/models/vorta_login_otps.js
Normal 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;
|
||||
};
|
||||
71
backend/src/db/models/vorta_social_comments.js
Normal file
71
backend/src/db/models/vorta_social_comments.js
Normal 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;
|
||||
};
|
||||
62
backend/src/db/models/vorta_social_posts.js
Normal file
62
backend/src/db/models/vorta_social_posts.js
Normal 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;
|
||||
};
|
||||
75
backend/src/db/models/vorta_universe_states.js
Normal file
75
backend/src/db/models/vorta_universe_states.js
Normal 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;
|
||||
};
|
||||
@ -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 }),
|
||||
|
||||
52
backend/src/routes/vortaCommerce.js
Normal file
52
backend/src/routes/vortaCommerce.js
Normal 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;
|
||||
71
backend/src/routes/vortaUniverse.js
Normal file
71
backend/src/routes/vortaUniverse.js
Normal 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;
|
||||
1466
backend/src/services/vortaCommerce.js
Normal file
1466
backend/src/services/vortaCommerce.js
Normal file
File diff suppressed because it is too large
Load Diff
763
backend/src/services/vortaUniverse.js
Normal file
763
backend/src/services/vortaUniverse.js
Normal 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;
|
||||
@ -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:
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
1368
frontend/src/pages/vorta-universe.tsx
Normal file
1368
frontend/src/pages/vorta-universe.tsx
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user