diff --git a/backend/src/db/api/users.js b/backend/src/db/api/users.js
index 63e8eab..57b5668 100644
--- a/backend/src/db/api/users.js
+++ b/backend/src/db/api/users.js
@@ -1,4 +1,3 @@
-
const db = require('../models');
const FileDBApi = require('./file');
const crypto = require('crypto');
@@ -32,7 +31,12 @@ module.exports = class UsersDBApi {
||
null
,
-
+ national_id: data.data.national_id || null,
+ address: data.data.address || null,
+ birth_day: data.data.birth_day || null,
+ birth_year: data.data.birth_year || null,
+ account_type: data.data.account_type || null,
+
phoneNumber: data.data.phoneNumber
||
null
@@ -109,7 +113,12 @@ module.exports = class UsersDBApi {
});
}
-
+ await users.setAccount_tier(data.data.account_tier || null, { transaction });
+ await users.setProvince(data.data.province || null, { transaction });
+ await users.setDistrict(data.data.district || null, { transaction });
+ await users.setSector(data.data.sector || null, { transaction });
+ await users.setCell(data.data.cell || null, { transaction });
+ await users.setVillage(data.data.village || null, { transaction });
await users.setCustom_permissions(data.data.custom_permissions || [], {
@@ -262,43 +271,27 @@ module.exports = class UsersDBApi {
const updatePayload = {};
if (data.firstName !== undefined) updatePayload.firstName = data.firstName;
-
-
if (data.lastName !== undefined) updatePayload.lastName = data.lastName;
-
-
+ if (data.national_id !== undefined) updatePayload.national_id = data.national_id;
+ if (data.address !== undefined) updatePayload.address = data.address;
+ if (data.birth_day !== undefined) updatePayload.birth_day = data.birth_day;
+ if (data.birth_year !== undefined) updatePayload.birth_year = data.birth_year;
+ if (data.account_type !== undefined) updatePayload.account_type = data.account_type;
+
if (data.phoneNumber !== undefined) updatePayload.phoneNumber = data.phoneNumber;
-
-
if (data.email !== undefined) updatePayload.email = data.email;
-
-
if (data.disabled !== undefined) updatePayload.disabled = data.disabled;
-
-
if (data.password !== undefined) updatePayload.password = data.password;
-
if (data.emailVerified !== undefined) updatePayload.emailVerified = data.emailVerified;
-
else updatePayload.emailVerified = true;
-
if (data.emailVerificationToken !== undefined) updatePayload.emailVerificationToken = data.emailVerificationToken;
-
-
if (data.emailVerificationTokenExpiresAt !== undefined) updatePayload.emailVerificationTokenExpiresAt = data.emailVerificationTokenExpiresAt;
-
-
if (data.passwordResetToken !== undefined) updatePayload.passwordResetToken = data.passwordResetToken;
-
-
if (data.passwordResetTokenExpiresAt !== undefined) updatePayload.passwordResetTokenExpiresAt = data.passwordResetTokenExpiresAt;
-
-
if (data.provider !== undefined) updatePayload.provider = data.provider;
-
updatePayload.updatedById = currentUser.id;
await users.update(updatePayload, {transaction});
@@ -313,6 +306,13 @@ module.exports = class UsersDBApi {
{ transaction }
);
}
+
+ if (data.account_tier !== undefined) await users.setAccount_tier(data.account_tier, { transaction });
+ if (data.province !== undefined) await users.setProvince(data.province, { transaction });
+ if (data.district !== undefined) await users.setDistrict(data.district, { transaction });
+ if (data.sector !== undefined) await users.setSector(data.sector, { transaction });
+ if (data.cell !== undefined) await users.setCell(data.cell, { transaction });
+ if (data.village !== undefined) await users.setVillage(data.village, { transaction });
@@ -363,7 +363,7 @@ module.exports = class UsersDBApi {
});
- return users;
+ return posts;
}
static async remove(id, options) {
@@ -462,6 +462,13 @@ module.exports = class UsersDBApi {
output.app_role = await users.getApp_role({
transaction
});
+
+ output.account_tier = await users.getAccount_tier({ transaction });
+ output.province = await users.getProvince({ transaction });
+ output.district = await users.getDistrict({ transaction });
+ output.sector = await users.getSector({ transaction });
+ output.cell = await users.getCell({ transaction });
+ output.village = await users.getVillage({ transaction });
if (output.app_role) {
output.app_role_permissions = await output.app_role.getPermissions({
@@ -973,5 +980,4 @@ module.exports = class UsersDBApi {
-};
-
+};
\ No newline at end of file
diff --git a/backend/src/db/migrations/1770756403378.js b/backend/src/db/migrations/1770756403378.js
new file mode 100644
index 0000000..9957886
--- /dev/null
+++ b/backend/src/db/migrations/1770756403378.js
@@ -0,0 +1,82 @@
+module.exports = {
+ async up(queryInterface, Sequelize) {
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.addColumn(
+ 'users',
+ 'national_id',
+ {
+ type: Sequelize.DataTypes.TEXT,
+ allowNull: true,
+ },
+ { transaction }
+ );
+ await queryInterface.addColumn(
+ 'users',
+ 'address',
+ {
+ type: Sequelize.DataTypes.TEXT,
+ allowNull: true,
+ },
+ { transaction }
+ );
+ await queryInterface.addColumn(
+ 'users',
+ 'birth_day',
+ {
+ type: Sequelize.DataTypes.INTEGER,
+ allowNull: true,
+ },
+ { transaction }
+ );
+ await queryInterface.addColumn(
+ 'users',
+ 'birth_year',
+ {
+ type: Sequelize.DataTypes.INTEGER,
+ allowNull: true,
+ },
+ { transaction }
+ );
+ await queryInterface.addColumn(
+ 'users',
+ 'account_type',
+ {
+ type: Sequelize.DataTypes.ENUM('Worker', 'Employer', 'Business', 'Government', 'NGO'),
+ allowNull: true,
+ },
+ { transaction }
+ );
+ await queryInterface.addColumn(
+ 'users',
+ 'wallet_balance_rwf',
+ {
+ type: Sequelize.DataTypes.DECIMAL,
+ defaultValue: 0,
+ allowNull: false,
+ },
+ { transaction }
+ );
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+
+ async down(queryInterface, Sequelize) {
+ const transaction = await queryInterface.sequelize.transaction();
+ try {
+ await queryInterface.removeColumn('users', 'national_id', { transaction });
+ await queryInterface.removeColumn('users', 'address', { transaction });
+ await queryInterface.removeColumn('users', 'birth_day', { transaction });
+ await queryInterface.removeColumn('users', 'birth_year', { transaction });
+ await queryInterface.removeColumn('users', 'account_type', { transaction });
+ await queryInterface.removeColumn('users', 'wallet_balance_rwf', { transaction });
+ await transaction.commit();
+ } catch (err) {
+ await transaction.rollback();
+ throw err;
+ }
+ },
+};
\ No newline at end of file
diff --git a/backend/src/db/models/users.js b/backend/src/db/models/users.js
index 0305f10..9994d0e 100644
--- a/backend/src/db/models/users.js
+++ b/backend/src/db/models/users.js
@@ -28,6 +28,27 @@ lastName: {
},
+national_id: {
+ type: DataTypes.TEXT,
+ },
+
+address: {
+ type: DataTypes.TEXT,
+ },
+
+birth_day: {
+ type: DataTypes.INTEGER,
+ },
+
+birth_year: {
+ type: DataTypes.INTEGER,
+ },
+
+account_type: {
+ type: DataTypes.ENUM,
+ values: ['Worker', 'Employer', 'Business', 'Government', 'NGO'],
+ },
+
phoneNumber: {
type: DataTypes.TEXT,
@@ -104,6 +125,12 @@ provider: {
},
+wallet_balance_rwf: {
+ type: DataTypes.DECIMAL,
+ allowNull: false,
+ defaultValue: 0,
+ },
+
importHash: {
type: DataTypes.STRING(255),
allowNull: true,
@@ -239,6 +266,53 @@ provider: {
constraints: false,
});
+ db.users.belongsTo(db.account_tiers, {
+ as: 'account_tier',
+ foreignKey: {
+ name: 'account_tierId',
+ },
+ constraints: false,
+ });
+
+ db.users.belongsTo(db.provinces, {
+ as: 'province',
+ foreignKey: {
+ name: 'provinceId',
+ },
+ constraints: false,
+ });
+
+ db.users.belongsTo(db.districts, {
+ as: 'district',
+ foreignKey: {
+ name: 'districtId',
+ },
+ constraints: false,
+ });
+
+ db.users.belongsTo(db.sectors, {
+ as: 'sector',
+ foreignKey: {
+ name: 'sectorId',
+ },
+ constraints: false,
+ });
+
+ db.users.belongsTo(db.cells, {
+ as: 'cell',
+ foreignKey: {
+ name: 'cellId',
+ },
+ constraints: false,
+ });
+
+ db.users.belongsTo(db.villages, {
+ as: 'village',
+ foreignKey: {
+ name: 'villageId',
+ },
+ constraints: false,
+ });
db.users.hasMany(db.file, {
@@ -304,5 +378,4 @@ function trimStringFields(users) {
: null;
return users;
-}
-
+}
\ No newline at end of file
diff --git a/backend/src/index.js b/backend/src/index.js
index 648d2e8..2966143 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -1,4 +1,3 @@
-
const express = require('express');
const cors = require('cors');
const app = express();
@@ -63,6 +62,7 @@ const help_center_articlesRoutes = require('./routes/help_center_articles');
const for_you_rankingsRoutes = require('./routes/for_you_rankings');
+const walletRoutes = require('./routes/wallet');
const getBaseUrl = (url) => {
if (!url) return '';
@@ -118,6 +118,7 @@ app.use(bodyParser.json());
app.use('/api/auth', authRoutes);
app.use('/api/file', fileRoutes);
app.use('/api/pexels', pexelsRoutes);
+app.use('/api/landing', require('./routes/landing'));
app.enable('trust proxy');
@@ -163,6 +164,8 @@ app.use('/api/help_center_articles', passport.authenticate('jwt', {session: fals
app.use('/api/for_you_rankings', passport.authenticate('jwt', {session: false}), for_you_rankingsRoutes);
+app.use('/api/wallet', walletRoutes);
+
app.use(
'/api/openai',
passport.authenticate('jwt', { session: false }),
@@ -207,4 +210,4 @@ db.sequelize.sync().then(function () {
});
});
-module.exports = app;
+module.exports = app;
\ No newline at end of file
diff --git a/backend/src/routes/landing.js b/backend/src/routes/landing.js
new file mode 100644
index 0000000..324d38a
--- /dev/null
+++ b/backend/src/routes/landing.js
@@ -0,0 +1,21 @@
+
+const express = require('express');
+const router = express.Router();
+const PostsDBApi = require('../db/api/posts');
+const AccountTiersDBApi = require('../db/api/account_tiers');
+const wrapAsync = require('../helpers').wrapAsync;
+
+router.get('/data', wrapAsync(async (req, res) => {
+ // Fetch latest posts for guest users
+ const posts = await PostsDBApi.findAll({ limit: 6, page: 0 }, { currentUser: null });
+
+ // Fetch account tiers
+ const tiers = await AccountTiersDBApi.findAll({ limit: 10, page: 0 }, { currentUser: null });
+
+ res.status(200).send({
+ posts: posts.rows,
+ tiers: tiers.rows
+ });
+}));
+
+module.exports = router;
diff --git a/backend/src/routes/wallet.js b/backend/src/routes/wallet.js
new file mode 100644
index 0000000..901119c
--- /dev/null
+++ b/backend/src/routes/wallet.js
@@ -0,0 +1,66 @@
+const express = require('express');
+const router = express.Router();
+const passport = require('passport');
+const db = require('../db/models');
+const { wrapAsync } = require('../helpers');
+
+router.post(
+ '/deposit',
+ passport.authenticate('jwt', { session: false }),
+ wrapAsync(async (req, res) => {
+ const { amount } = req.body;
+ const userId = req.user.id;
+
+ if (!amount || isNaN(amount) || amount <= 0) {
+ return res.status(400).send({ message: 'Invalid amount' });
+ }
+
+ const transaction = await db.sequelize.transaction();
+
+ try {
+ const user = await db.users.findByPk(userId, { transaction });
+
+ const balanceBefore = parseFloat(user.wallet_balance_rwf || 0);
+ const depositAmount = parseFloat(amount);
+ const balanceAfter = balanceBefore + depositAmount;
+
+ await user.update({ wallet_balance_rwf: balanceAfter }, { transaction });
+
+ await db.wallet_transactions.create({
+ userId,
+ type: 'deposit',
+ status: 'completed',
+ amount_rwf: depositAmount,
+ balance_before_rwf: balanceBefore,
+ balance_after_rwf: balanceAfter,
+ reference_code: `DEP-${Date.now()}`,
+ reason: 'Manual simulation deposit',
+ processed_at: new Date(),
+ createdById: userId,
+ updatedById: userId,
+ }, { transaction });
+
+ await transaction.commit();
+
+ res.status(200).send({
+ message: 'Deposit successful',
+ balance: balanceAfter
+ });
+ } catch (error) {
+ await transaction.rollback();
+ console.error('Deposit error:', error);
+ res.status(500).send({ message: 'Internal server error' });
+ }
+ })
+);
+
+router.get(
+ '/balance',
+ passport.authenticate('jwt', { session: false }),
+ wrapAsync(async (req, res) => {
+ const user = await db.users.findByPk(req.user.id);
+ res.status(200).send({ balance: user.wallet_balance_rwf });
+ })
+);
+
+module.exports = router;
diff --git a/frontend/src/components/NavBar.tsx b/frontend/src/components/NavBar.tsx
index c270ae0..455120f 100644
--- a/frontend/src/components/NavBar.tsx
+++ b/frontend/src/components/NavBar.tsx
@@ -1,11 +1,12 @@
import React, { ReactNode, useState, useEffect } from 'react'
-import { mdiClose, mdiDotsVertical } from '@mdi/js'
+import { mdiClose, mdiDotsVertical, mdiWallet } from '@mdi/js'
import { containerMaxW } from '../config'
import BaseIcon from './BaseIcon'
import NavBarItemPlain from './NavBarItemPlain'
import NavBarMenuList from './NavBarMenuList'
import { MenuNavBarItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks';
+import Link from 'next/link';
type Props = {
menu: MenuNavBarItem[]
@@ -17,6 +18,7 @@ export default function NavBar({ menu, className = '', children }: Props) {
const [isMenuNavBarActive, setIsMenuNavBarActive] = useState(false)
const [isScrolled, setIsScrolled] = useState(false);
const bgColor = useAppSelector((state) => state.style.bgLayoutColor);
+ const { currentUser } = useAppSelector((state) => state.auth);
useEffect(() => {
const handleScroll = () => {
@@ -39,6 +41,16 @@ export default function NavBar({ menu, className = '', children }: Props) {
>
{children}
+
+ {currentUser && (
+
+
+
+ 0 RWF
+
+
+ )}
+
@@ -54,4 +66,4 @@ export default function NavBar({ menu, className = '', children }: Props) {
)
-}
+}
\ No newline at end of file
diff --git a/frontend/src/components/NavBarItem.tsx b/frontend/src/components/NavBarItem.tsx
index 72935e6..4ced3eb 100644
--- a/frontend/src/components/NavBarItem.tsx
+++ b/frontend/src/components/NavBarItem.tsx
@@ -1,6 +1,5 @@
-import React, {useEffect, useRef} from 'react'
+import React, {useEffect, useRef, useState} from 'react'
import Link from 'next/link'
-import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon'
@@ -129,4 +128,4 @@ export default function NavBarItem({ item }: Props) {
}
return
{NavBarItemComponentContents}
-}
+}
\ No newline at end of file
diff --git a/frontend/src/components/UserAvatarWithBadge.tsx b/frontend/src/components/UserAvatarWithBadge.tsx
new file mode 100644
index 0000000..2e93def
--- /dev/null
+++ b/frontend/src/components/UserAvatarWithBadge.tsx
@@ -0,0 +1,48 @@
+
+import React from 'react';
+import { mdiAccount, mdiImageOutline } from '@mdi/js';
+import BaseIcon from './BaseIcon';
+
+type Props = {
+ user: any;
+ className?: string;
+ size?: number;
+};
+
+export default function UserAvatarWithBadge({ user, className = '', size = 48 }: Props) {
+ const avatarUrl = user?.avatar && user.avatar[0] ? user.avatar[0].publicUrl : null;
+ const tierName = user?.account_tier?.name?.toLowerCase() || '';
+
+ const getBadgeColor = (name: string) => {
+ if (name.includes('plutonium')) return 'bg-purple-600';
+ if (name.includes('platinum')) return 'bg-blue-600';
+ if (name.includes('gold')) return 'bg-yellow-500';
+ if (name.includes('silver')) return 'bg-slate-400';
+ if (name.includes('bronze')) return 'bg-orange-500';
+ return null;
+ };
+
+ const badgeColor = getBadgeColor(tierName);
+
+ return (
+
+ {avatarUrl ? (
+

+ ) : (
+
+
+
+ )}
+
+ {badgeColor && (
+
+ {user.account_tier.name.substring(0, 1)}
+
+ )}
+
+ );
+}
diff --git a/frontend/src/components/WalletCard.tsx b/frontend/src/components/WalletCard.tsx
new file mode 100644
index 0000000..d30fcf5
--- /dev/null
+++ b/frontend/src/components/WalletCard.tsx
@@ -0,0 +1,95 @@
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import { mdiWallet, mdiPlusCircle } from '@mdi/js';
+import BaseIcon from './BaseIcon';
+import CardBox from './CardBox';
+import BaseButton from './BaseButton';
+import CardBoxModal from './CardBoxModal';
+import FormField from './FormField';
+
+const WalletCard = () => {
+ const [balance, setBalance] = useState
(0);
+ const [isModalOpen, setIsModalOpen] = useState(false);
+ const [depositAmount, setDepositAmount] = useState('1000');
+ const [isLoading, setIsLoading] = useState(false);
+
+ const fetchBalance = async () => {
+ try {
+ const response = await axios.get('/wallet/balance');
+ setBalance(Number(response.data.balance) || 0);
+ } catch (error) {
+ console.error('Failed to fetch balance:', error);
+ }
+ };
+
+ useEffect(() => {
+ fetchBalance();
+ }, []);
+
+ const handleDeposit = async () => {
+ setIsLoading(true);
+ try {
+ await axios.post('/wallet/deposit', { amount: Number(depositAmount) });
+ await fetchBalance();
+ setIsModalOpen(false);
+ } catch (error) {
+ console.error('Deposit failed:', error);
+ alert('Deposit failed. Please try again.');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+ <>
+ setIsModalOpen(false)}
+ isLoading={isLoading}
+ >
+
+ This is a Wallet Simulation. No real money will be charged.
+ Enter the amount in RWF you want to add to your MENYAHAFI account.
+
+
+ setDepositAmount(e.target.value)}
+ className="px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full bg-white"
+ />
+
+
+
+
+
+
+
+
+
+
+
Your Balance
+
+ {balance.toLocaleString()} RWF
+
+
+
+
+
+
+ >
+ );
+};
+
+export default WalletCard;
diff --git a/frontend/src/layouts/Authenticated.tsx b/frontend/src/layouts/Authenticated.tsx
index 1b9907d..26c3572 100644
--- a/frontend/src/layouts/Authenticated.tsx
+++ b/frontend/src/layouts/Authenticated.tsx
@@ -1,5 +1,4 @@
-import React, { ReactNode, useEffect } from 'react'
-import { useState } from 'react'
+import React, { ReactNode, useEffect, useState } from 'react'
import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside'
@@ -126,4 +125,4 @@ export default function LayoutAuthenticated({
)
-}
+}
\ No newline at end of file
diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx
index 7bd442e..d7ff4d2 100644
--- a/frontend/src/pages/dashboard.tsx
+++ b/frontend/src/pages/dashboard.tsx
@@ -11,748 +11,195 @@ import { getPageTitle } from '../config'
import Link from "next/link";
import { hasPermission } from "../helpers/userPermissions";
-import { fetchWidgets } from '../stores/roles/rolesSlice';
-import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
-import { SmartWidget } from '../components/SmartWidget/SmartWidget';
-
import { useAppDispatch, useAppSelector } from '../stores/hooks';
+import CardBox from '../components/CardBox';
+import BaseButton from '../components/BaseButton';
+import BaseButtons from '../components/BaseButtons';
+import UserAvatarWithBadge from '../components/UserAvatarWithBadge';
+
const Dashboard = () => {
- const dispatch = useAppDispatch();
const iconsColor = useAppSelector((state) => state.style.iconsColor);
- const corners = useAppSelector((state) => state.style.corners);
- const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
-
- const loadingMessage = 'Loading...';
-
-
- const [users, setUsers] = React.useState(loadingMessage);
- const [roles, setRoles] = React.useState(loadingMessage);
- const [permissions, setPermissions] = React.useState(loadingMessage);
- const [provinces, setProvinces] = React.useState(loadingMessage);
- const [districts, setDistricts] = React.useState(loadingMessage);
- const [sectors, setSectors] = React.useState(loadingMessage);
- const [cells, setCells] = React.useState(loadingMessage);
- const [villages, setVillages] = React.useState(loadingMessage);
- const [account_tiers, setAccount_tiers] = React.useState(loadingMessage);
- const [subscriptions, setSubscriptions] = React.useState(loadingMessage);
- const [wallet_transactions, setWallet_transactions] = React.useState(loadingMessage);
- const [content_categories, setContent_categories] = React.useState(loadingMessage);
- const [posts, setPosts] = React.useState(loadingMessage);
- const [paid_actions, setPaid_actions] = React.useState(loadingMessage);
- const [engagement_events, setEngagement_events] = React.useState(loadingMessage);
- const [fee_rules, setFee_rules] = React.useState(loadingMessage);
- const [rule_engine_events, setRule_engine_events] = React.useState(loadingMessage);
- const [notifications, setNotifications] = React.useState(loadingMessage);
- const [admin_settings, setAdmin_settings] = React.useState(loadingMessage);
- const [help_center_articles, setHelp_center_articles] = React.useState(loadingMessage);
- const [for_you_rankings, setFor_you_rankings] = React.useState(loadingMessage);
-
-
- const [widgetsRole, setWidgetsRole] = React.useState({
- role: { value: '', label: '' },
- });
const { currentUser } = useAppSelector((state) => state.auth);
- const { isFetchingQuery } = useAppSelector((state) => state.openAi);
-
- const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
-
-
- async function loadData() {
- const entities = ['users','roles','permissions','provinces','districts','sectors','cells','villages','account_tiers','subscriptions','wallet_transactions','content_categories','posts','paid_actions','engagement_events','fee_rules','rule_engine_events','notifications','admin_settings','help_center_articles','for_you_rankings',];
- const fns = [setUsers,setRoles,setPermissions,setProvinces,setDistricts,setSectors,setCells,setVillages,setAccount_tiers,setSubscriptions,setWallet_transactions,setContent_categories,setPosts,setPaid_actions,setEngagement_events,setFee_rules,setRule_engine_events,setNotifications,setAdmin_settings,setHelp_center_articles,setFor_you_rankings,];
- const requests = entities.map((entity, index) => {
-
- if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
- return axios.get(`/${entity.toLowerCase()}/count`);
- } else {
- fns[index](null);
- return Promise.resolve({data: {count: null}});
- }
-
- });
+ const [stats, setStats] = React.useState({
+ posts: 0,
+ wallet_balance: 0,
+ paid_actions: 0,
+ notifications: 0
+ });
- Promise.allSettled(requests).then((results) => {
- results.forEach((result, i) => {
- if (result.status === 'fulfilled') {
- fns[i](result.value.data.count);
- } else {
- fns[i](result.reason.message);
- }
+ async function loadStats() {
+ try {
+ const [postsCount, actionsCount, notifsCount] = await Promise.all([
+ axios.get('/posts/count'),
+ axios.get('/paid_actions/count'),
+ axios.get('/notifications/count')
+ ]);
+ setStats({
+ posts: postsCount.data.count,
+ wallet_balance: 0, // Placeholder as wallet logic is simulated
+ paid_actions: actionsCount.data.count,
+ notifications: notifsCount.data.count
});
- });
- }
-
- async function getWidgets(roleId) {
- await dispatch(fetchWidgets(roleId));
+ } catch (e) {
+ console.error(e);
+ }
}
+
React.useEffect(() => {
- if (!currentUser) return;
- loadData().then();
- setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
+ if (currentUser) loadStats();
}, [currentUser]);
- React.useEffect(() => {
- if (!currentUser || !widgetsRole?.role?.value) return;
- getWidgets(widgetsRole?.role?.value || '').then();
- }, [widgetsRole?.role?.value]);
-
- return (
- <>
-
-
- {getPageTitle('Overview')}
-
-
-
-
- {''}
-
-
- {hasPermission(currentUser, 'CREATE_ROLES') && }
- {!!rolesWidgets.length &&
- hasPermission(currentUser, 'CREATE_ROLES') && (
-
- {`${widgetsRole?.role?.label || 'Users'}'s widgets`}
-
- )}
-
-
- {(isFetchingQuery || loading) && (
-
- {' '}
- Loading widgets...
-
- )}
-
- { rolesWidgets &&
- rolesWidgets.map((widget) => (
-
+
+ {getPageTitle('Menyahafi Dashboard')}
+
+
+
+
- ))}
-
+
- {!!rolesWidgets.length &&
}
-
-
-
-
- {hasPermission(currentUser, 'READ_USERS') &&
-
-
-
-
- Users
-
-
- {users}
-
+ {/* User Status Card */}
+
+
+
+
+
{currentUser?.firstName}, Welcome back!
+
+ Current Tier: {currentUser?.account_tier?.name || 'Starter'} •
+ Location: {currentUser?.village?.name || 'Kigali'}
+
-
-
+
+
Wallet Balance
+
{stats.wallet_balance} RWF
+
+ Deposit Funds
+
+
+
+
+
+
+
+
+
+
My Posts
+
{stats.posts}
+
+
+
+
+
+
+
+
Paid Actions
+
{stats.paid_actions}
+
+
+
+
+
+
+
+
Alerts
+
{stats.notifications}
+
+
+
+
+
- }
-
- {hasPermission(currentUser, 'READ_ROLES') &&
-
-
-
-
- Roles
-
-
- {roles}
-
-
-
+
+
+
+
+
- }
-
- {hasPermission(currentUser, 'READ_PERMISSIONS') &&
-
-
-
-
- Permissions
-
-
- {permissions}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PROVINCES') &&
-
-
-
-
- Provinces
-
-
- {provinces}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_DISTRICTS') &&
-
-
-
-
- Districts
-
-
- {districts}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_SECTORS') &&
-
-
-
-
- Sectors
-
-
- {sectors}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_CELLS') &&
-
-
-
-
- Cells
-
-
- {cells}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_VILLAGES') &&
-
-
-
-
- Villages
-
-
- {villages}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_ACCOUNT_TIERS') &&
-
-
-
-
- Account tiers
-
-
- {account_tiers}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_SUBSCRIPTIONS') &&
-
-
-
-
- Subscriptions
-
-
- {subscriptions}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_WALLET_TRANSACTIONS') &&
-
-
-
-
- Wallet transactions
-
-
- {wallet_transactions}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_CONTENT_CATEGORIES') &&
-
-
-
-
- Content categories
-
-
- {content_categories}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_POSTS') &&
-
-
-
-
- Posts
-
-
- {posts}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PAID_ACTIONS') &&
-
-
-
-
- Paid actions
-
-
- {paid_actions}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_ENGAGEMENT_EVENTS') &&
-
-
-
-
- Engagement events
-
-
- {engagement_events}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_FEE_RULES') &&
-
-
-
-
- Fee rules
-
-
- {fee_rules}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_RULE_ENGINE_EVENTS') &&
-
-
-
-
- Rule engine events
-
-
- {rule_engine_events}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_NOTIFICATIONS') &&
-
-
-
-
- Notifications
-
-
- {notifications}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_ADMIN_SETTINGS') &&
-
-
-
-
- Admin settings
-
-
- {admin_settings}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_HELP_CENTER_ARTICLES') &&
-
-
-
-
- Help center articles
-
-
- {help_center_articles}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_FOR_YOU_RANKINGS') &&
-
-
-
-
- For you rankings
-
-
- {for_you_rankings}
-
-
-
-
-
-
-
- }
-
-
-
-
- >
- )
+
+ >
+ );
}
Dashboard.getLayout = function getLayout(page: ReactElement) {
- return
{page}
+ return
{page}
}
-export default Dashboard
+export default Dashboard;
\ No newline at end of file
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index 3aef749..7279791 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -1,166 +1,423 @@
-
import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
-import BaseButton from '../components/BaseButton';
-import CardBox from '../components/CardBox';
-import SectionFullScreen from '../components/SectionFullScreen';
+import axios from 'axios';
+import SectionMain from '../components/SectionMain';
import LayoutGuest from '../layouts/Guest';
-import BaseDivider from '../components/BaseDivider';
-import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
import { useAppSelector } from '../stores/hooks';
-import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
-import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
+import { mdiWallet, mdiMapMarker, mdiStar, mdiBriefcase, mdiBullhorn, mdiChevronRight, mdiShieldCheck, mdiAlert } from '@mdi/js';
+import BaseIcon from '../components/BaseIcon';
+import WalletCard from '../components/WalletCard';
+import FormField from '../components/FormField';
+export default function MenyahafiLanding() {
+ const [posts, setPosts] = useState([]);
+ const [tiers, setTiers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const { currentUser } = useAppSelector((state) => state.auth);
+
+ // Demo Post State
+ const [demoPostType, setDemoPostType] = useState('job_daily');
+ const [demoTitle, setDemoTitle] = useState('');
+ const [demoFee, setDemoFee] = useState(500);
+ const [userBalance, setUserBalance] = useState(0);
-export default function Starter() {
- const [illustrationImage, setIllustrationImage] = useState({
- src: undefined,
- photographer: undefined,
- photographer_url: undefined,
- })
- const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
- const [contentType, setContentType] = useState('image');
- const [contentPosition, setContentPosition] = useState('right');
- const textColor = useAppSelector((state) => state.style.linkColor);
-
- const title = 'MENYAHAFI'
-
- // Fetch Pexels image/video
useEffect(() => {
- async function fetchData() {
- const image = await getPexelsImage();
- const video = await getPexelsVideo();
- setIllustrationImage(image);
- setIllustrationVideo(video);
+ const fetchLandingData = async () => {
+ try {
+ const response = await axios.get('/landing/data');
+ setPosts(response.data.posts || []);
+ setTiers(response.data.tiers || []);
+ } catch (error) {
+ console.error('Failed to fetch landing data:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+ fetchLandingData();
+
+ if (currentUser) {
+ axios.get('/wallet/balance').then(res => {
+ setUserBalance(Number(res.data.balance) || 0);
+ });
}
- fetchData();
- }, []);
+ }, [currentUser]);
- const imageBlock = (image) => (
-
-
-
- Photo by {image?.photographer} on Pexels
-
+ useEffect(() => {
+ // Simple fee logic for demo
+ let baseFee = 500;
+ if (demoPostType === 'job_monthly') baseFee = 2000;
+ if (demoPostType.startsWith('ad_')) baseFee = 3500;
+ if (demoPostType === 'nyakabyizi') baseFee = 0;
+
+ // AI detection of links (mock)
+ const hasLinks = /http|www|\.com|\.rw|07[8923]\d{7}/.test(demoTitle);
+ if (hasLinks) baseFee += 1500;
+
+ setDemoFee(baseFee);
+ }, [demoPostType, demoTitle]);
+
+ const defaultTiers = [
+ { name: 'Starter', price: '0', color: 'bg-gray-100', text: 'text-gray-800' },
+ { name: 'Bronze', price: '1,000', color: 'bg-orange-100', text: 'text-orange-800' },
+ { name: 'Silver', price: '3,000', color: 'bg-slate-200', text: 'text-slate-800' },
+ { name: 'Gold', price: '6,000', color: 'bg-yellow-100', text: 'text-yellow-800' },
+ { name: 'Platinum', price: '10,000', color: 'bg-blue-100', text: 'text-blue-800' },
+ { name: 'Plutonium', price: '15,000', color: 'bg-purple-100', text: 'text-purple-800' },
+ ];
+
+ const displayTiers = tiers.length > 0 ? tiers.map((t: any) => ({
+ name: t.name,
+ price: t.subscription_fee_rwf,
+ color: t.name.toLowerCase().includes('platinum') ? 'bg-blue-100' : (t.name.toLowerCase().includes('gold') ? 'bg-yellow-100' : 'bg-gray-100'),
+ text: 'text-gray-800'
+ })) : defaultTiers;
+
+ const isBalanceInsufficient = currentUser && userBalance < demoFee;
+
+ return (
+
+
+
{getPageTitle('MENYAHAFI - Paid Digital Marketplace Rwanda')}
+
+
+ {/* Hero Section */}
+
+
+
+
+
+
+ 100% Automated Trusted Platform
+
+
+ MENYAHAFI
+
+
+ The heartbeat of Rwanda's digital economy.
+ Safe jobs, professional ads, and verified local engagement.
+
+
+ {!currentUser ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : (
+
+
+
+ )}
+
+
+
+
+ {/* Stats / Wallet Promo */}
+
+ {currentUser ? (
+
+ ) : (
+
+
+
+
+
+
+
Your Wallet
+
Login to View
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
Verified Ads
+
1,200+
+
+
+
+
+
+ {/* AI Fee Engine Demo */}
+
+
+
+
+
+ Automatic Fee Engine
+
+
+ Test Our AI-Powered
Pricing Real-Time
+
+
+ See how our rules engine calculates fees based on category,
+ content detection (links/scams), and your location.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Calculated Fee
+
+ {demoFee.toLocaleString()} RWF
+
+ {demoTitle.includes('07') || demoTitle.includes('http') ? (
+
+ LINK DETECTED: +1,500 RWF Fee
+
+ ) : null}
+
+
+
+ {isBalanceInsufficient ? (
+
+
+
+ Locked!
+
+
+ “Ntushobora gukomeza. Banza wongere amafaranga kuri konti yawe.”
+
+
+ Your balance ({userBalance} RWF) is too low.
+
+
+ ) : (
+
+
+ {currentUser ? 'Balance Sufficient!' : 'Login to Post'}
+
+
+ )}
+
+
+
+
+
+
+
+ {/* For You Feed */}
+
+
+
+
+
+ For You
+
+
Personalized Marketplace Ranking
+
+
+ View All
+
+
+
+ {loading ? (
+
+ {[1, 2, 3].map((i) => (
+
+ ))}
+
+ ) : (
+
+ {posts.length > 0 ? posts.map((post: any) => (
+
+
+ {post.images && post.images.length > 0 ? (
+

+ ) : (
+
+
+
+ )}
+
+
+ {post.post_type?.replace('_', ' ')}
+
+
+ {post.is_boosted && (
+
+
+ BOOSTED
+
+
+ )}
+
+
+
+ {post.title}
+
+
+
+ {post.village?.name || 'Local Village'}
+
+
+
+
Fee To Reveal
+
+ {post.required_fee_rwf ? `${post.required_fee_rwf}` : 'FREE'}
+ {post.required_fee_rwf ? RWF : ''}
+
+
+
+
+
+
+ )) : (
+
+
+
+
+
No posts in your area yet.
+
Be the first to post a job or ad and start growing your business today!
+
+ )}
+
+ )}
+
+
+ {/* Tiers Section */}
+
+
+
+
+
+
Choose Your Power
+
Unlock higher visibility and exclusive discounts.
+
+
+ {displayTiers.map((tier) => (
+
+
+
+
{tier.price} RWF
+
Per Month
+
+
+ ))}
+
+
+
+
+
+ {/* Footer */}
+
);
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
-
)
- }
- };
-
- return (
-
-
-
{getPageTitle('Starter Page')}
-
-
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
© 2026 {title}. All rights reserved
-
- Privacy Policy
-
-
-
-
- );
}
-Starter.getLayout = function getLayout(page: ReactElement) {
- return
{page};
-};
-
+MenyahafiLanding.getLayout = function getLayout(page: ReactElement) {
+ return
{page};
+};
\ No newline at end of file
diff --git a/frontend/src/pages/profile.tsx b/frontend/src/pages/profile.tsx
index f5eb7cf..00c1e47 100644
--- a/frontend/src/pages/profile.tsx
+++ b/frontend/src/pages/profile.tsx
@@ -1,6 +1,7 @@
import {
mdiChartTimelineVariant,
mdiUpload,
+ mdiAccountDetails,
} from '@mdi/js';
import Head from 'next/head';
import React, { ReactElement, useEffect, useState } from 'react';
@@ -29,6 +30,7 @@ import { update, fetch } from '../stores/users/usersSlice';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
import { useRouter } from 'next/router';
import {findMe} from "../stores/authSlice";
+import UserAvatarWithBadge from '../components/UserAvatarWithBadge';
const EditUsers = () => {
const { currentUser, isFetching, token } = useAppSelector(
@@ -45,7 +47,18 @@ const EditUsers = () => {
app_role: '',
disabled: false,
avatar: [],
- password: ''
+ password: '',
+ national_id: '',
+ address: '',
+ birth_day: '',
+ birth_year: '',
+ account_type: '',
+ account_tier: '',
+ province: '',
+ district: '',
+ sector: '',
+ cell: '',
+ village: '',
};
const [initialValues, setInitialValues] = useState(initVals);
@@ -54,7 +67,15 @@ const EditUsers = () => {
const newInitialVal = { ...initVals };
Object.keys(initVals).forEach(
- (el) => (newInitialVal[el] = currentUser[el]),
+ (el) => {
+ if (currentUser[el]) {
+ if (typeof currentUser[el] === 'object' && currentUser[el].id) {
+ newInitialVal[el] = currentUser[el].id;
+ } else {
+ newInitialVal[el] = currentUser[el];
+ }
+ }
+ }
);
setInitialValues(newInitialVal);
@@ -64,7 +85,6 @@ const EditUsers = () => {
const handleSubmit = async (data) => {
await dispatch(update({ id: currentUser.id, data }));
await dispatch(findMe());
- await router.push('/users/users-list');
notify('success', 'Profile was updated!');
};
@@ -75,46 +95,88 @@ const EditUsers = () => {
{''}
- {currentUser?.avatar[0]?.publicUrl &&
-
-

-
-
}
+
+
+
{currentUser?.firstName} {currentUser?.lastName}
+
{currentUser?.account_tier?.name || 'Starter'}
+
+
handleSubmit(values)}
>
@@ -177,4 +201,4 @@ EditUsers.getLayout = function getLayout(page: ReactElement) {
return {page};
};
-export default EditUsers;
+export default EditUsers;
\ No newline at end of file