Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
867b8c3
feat: implement MasonryGrid component for responsive item layout and …
ZanDev32 Jan 9, 2026
f760197
fix: enhance item selection logic to avoid duplicates and improve end…
ZanDev32 Jan 9, 2026
da00951
fix: double slider visual problem
ZanDev32 Jan 9, 2026
5f24b41
style: override Mapbox popup styles for a cleaner look
ZanDev32 Jan 9, 2026
32fc517
fix: add crossOrigin attribute to images for better CORS handling
ZanDev32 Jan 9, 2026
8bb8c49
fix: update max-width values for responsive design in MapPlacePopup a…
ZanDev32 Jan 9, 2026
51a1600
feat: implement admin dashboard API, promotions, and user management
ZanDev32 Jan 9, 2026
2a098f5
feat: Implementing admin dashboard with new sections and improved nav…
ZanDev32 Jan 9, 2026
58ba93a
feat: Enhance admin routes with role-based access control and update …
ZanDev32 Jan 9, 2026
803f1f8
Feat: Implement traffic analytics, online presence tracking, and dash…
ZanDev32 Jan 9, 2026
7e84713
Feat: Add user deletion capability for admins
ZanDev32 Jan 9, 2026
2b1c159
Refactor: Implement database seeding for Ads and Env Users
ZanDev32 Jan 9, 2026
1428a54
Feat: Add password visibility toggle to auth forms
ZanDev32 Jan 9, 2026
eea4540
feat: Update endpoints for users, places, and promotions
ZanDev32 Jan 9, 2026
7a70a33
feat: add AdminModal and ConfirmationOverlay components
ZanDev32 Jan 9, 2026
7fbb0be
feat: implement edit functionality for users and ads
ZanDev32 Jan 9, 2026
5343f5a
feat: implement detailed place editing form with responsive layout
ZanDev32 Jan 9, 2026
0cbef5c
style: improve dashboard responsiveness and navigation elements
ZanDev32 Jan 9, 2026
d82d75f
style: add hover animation to profile button
ZanDev32 Jan 9, 2026
977daba
feat: enhance location update functionality with image handling and p…
ZanDev32 Jan 9, 2026
1135480
feat: update shared hooks and modal component styles
ZanDev32 Jan 9, 2026
e74ca3e
feat: implement admin reviews management with status workflow
ZanDev32 Jan 9, 2026
4eeaaac
feat: add avatar editing, role management, and sorting to admin users…
ZanDev32 Jan 9, 2026
0e20f86
feat: add type column and sorting options to admin places table
ZanDev32 Jan 9, 2026
906f6ba
feat: refactor ads storage to 'ads_images', add create capability, an…
ZanDev32 Jan 9, 2026
db3f977
feat: add detail view to reviews admin section
ZanDev32 Jan 9, 2026
29a89c1
fix: clean up modal button rendering when text is empty
ZanDev32 Jan 9, 2026
3a946b0
feat: integrate Docker management features in admin panel
ZanDev32 Jan 9, 2026
16eeb7e
feat: implement system alert management with CRUD operations and moni…
ZanDev32 Jan 9, 2026
92ead57
feat: enhance ControlSection UI with improved container status displa…
ZanDev32 Jan 9, 2026
a294f02
feat: update button styles across admin sections for improved UI cons…
ZanDev32 Jan 9, 2026
c5df74c
feat: enhance UI with new SearchTab component and animations for impr…
ZanDev32 Jan 10, 2026
28716c2
feat: enhance MapTab UI with improved styling and additional controls…
ZanDev32 Jan 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
425 changes: 409 additions & 16 deletions backend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"bcryptjs": "^3.0.3",
"body-parser": "^2.2.1",
"cors": "^2.8.5",
"dockerode": "^4.0.9",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"google-auth-library": "^10.5.0",
Expand Down
8 changes: 7 additions & 1 deletion backend/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ const bodyParser = require('body-parser');
const cors = require('cors');
const locationsRoutes = require('./routes/locations.routes');
const authRoutes = require('./routes/auth.routes');
const adminRoutes = require('./routes/admin.routes');
const promotionsRoutes = require('./routes/promotions.routes');
const contactRoutes = require('./routes/contact.routes');
const reviewsRoutes = require('./routes/reviews.routes');
const debugRoutes = require('./routes/debug.routes');
const errorHandler = require('./middleware/errorHandler');
const trackTraffic = require('./middleware/trackTraffic');

const app = express();

app.use(cors());
app.use(bodyParser.json({ limit: '10mb' }));
app.use(bodyParser.urlencoded({ extended: true, limit: '10mb' }));
app.use(trackTraffic);

// Serve dummy promoted ad assets (dev-friendly).
// Prefer frontend source assets; fallback to backend public folder if present.
Expand All @@ -31,14 +35,16 @@ const adsDir = adsDirCandidates.find((p) => {
});

if (adsDir) {
app.use('/api/static/ads', express.static(adsDir));
app.use('/api/static/ads_images', express.static(adsDir));
}

app.get('/api', (req, res) => {
res.status(200).send('Message from PlaceRadar Back-End using API');
});

app.use('/api/auth', authRoutes);
app.use('/api/admin', adminRoutes);
app.use('/api/promotions', promotionsRoutes);
app.use('/api/place', locationsRoutes);
app.use('/api/contact', contactRoutes);
app.use('/api/reviews', reviewsRoutes);
Expand Down
14 changes: 14 additions & 0 deletions backend/src/config/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -241,17 +241,31 @@ const connectDB = async () => {
require('../models/Admin');
require('../models/Location');
require('../models/Review');
require('../models/Promotion');
require('../models/PlaceFacility');
require('../models/OpeningHour');
require('../models/PlaceImage');
require('../models/UserFavorite');
require('../models/ProductivityVote');
require('../models/SysAlert');

await dropProductivityScoreIfPresent();
await sequelize.sync({ alter: true }); // Apply model changes to the database
await ensureProductivityScoreGeneratedColumn();
await ensureSearchSetup();
await ensureLocationGeometryAndIndex();

// Seed Data
try {
const seedPromotions = require('../seed/seedPromotions');
await seedPromotions();

const seedEnvUsers = require('../seed/seedEnvUsers');
await seedEnvUsers();
} catch (seedErr) {
console.error('Seeding failed:', seedErr);
}

console.log('Database synced');
} else {
console.log('Skipping sequelize.sync; set DB_SYNC_ON_BOOT=true to enable automatic sync.');
Expand Down
2 changes: 2 additions & 0 deletions backend/src/config/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ const env = {
JWT_EXPIRES_IN: process.env.JWT_EXPIRES_IN || '1h',
ADMIN_USERNAME: process.env.ADMIN_USERNAME || 'admin',
ADMIN_PASSWORD: process.env.ADMIN_PASSWORD || 'admin123',
EMAIL_USER: process.env.EMAIL_USER,
EMAIL_PASS: process.env.EMAIL_PASS,
DB_SYNC_ON_BOOT: (process.env.DB_SYNC_ON_BOOT || 'true').toLowerCase() === 'true',
GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
Expand Down
165 changes: 165 additions & 0 deletions backend/src/controllers/admin.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
const User = require('../models/User');
const Location = require('../models/Location');
const Promotion = require('../models/Promotion');
const DailyStat = require('../models/DailyStat');
const { Op } = require('sequelize');
const dockerService = require('../services/docker.service');

const getDashboardStats = async (req, res) => {
try {
const totalUsers = await User.count();
const totalPlaces = await Location.count();
const totalAds = await Promotion.count({ where: { active: true } });

// Fetch actual traffic data
const last7DaysStats = await DailyStat.findAll({
order: [['date', 'DESC']],
limit: 7,
raw: true
});

// Fill in missing days for the last 7 days for a complete chart
const weeklyTraffic = [];
const today = new Date();

for (let i = 6; i >= 0; i--) {
const d = new Date(today);
d.setDate(d.getDate() - i);
const dateStr = d.toISOString().split('T')[0];
const stat = last7DaysStats.find(s => {
const sDate = s.date instanceof Date
? s.date.toISOString().split('T')[0]
: s.date;
return sDate === dateStr;
});

weeklyTraffic.push({
name: d.toLocaleDateString('en-US', { weekday: 'short' }),
uv: stat ? stat.uniqueVisitors : 0,
pv: stat ? stat.requests : 0
});
}

const totalVisits = await DailyStat.sum('uniqueVisitors') || 0;
const totalPageViews = await DailyStat.sum('requests') || 0;
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable totalPageViews.

Copilot uses AI. Check for mistakes.

const todaysStat = last7DaysStats.find(s => {
const sDate = s.date instanceof Date
? s.date.toISOString().split('T')[0]
: s.date;
return sDate === today.toISOString().split('T')[0];
});
const dailyVisitors = todaysStat ? todaysStat.uniqueVisitors : 0;

// Online Members (Active in last 15 minutes)
const onlineMemberThreshold = new Date(Date.now() - 15 * 60 * 1000);
let onlineMembers = await User.count({
where: {
lastActive: {
[Op.gte]: onlineMemberThreshold
}
}
});

// Add Root Admin if active
if (global.lastRootAdminActive && global.lastRootAdminActive >= onlineMemberThreshold) {
onlineMembers++;
}

// Trends (Today vs Yesterday)
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const yesterdayStr = yesterday.toISOString().split('T')[0];

const yesterdaysStat = last7DaysStats.find(s => {
const sDate = s.date instanceof Date
? s.date.toISOString().split('T')[0]
: s.date;
return sDate === yesterdayStr;
});
const yesterdayVisitors = yesterdaysStat ? yesterdaysStat.uniqueVisitors : 0;

let trend = 0;
if (yesterdayVisitors > 0) {
trend = ((dailyVisitors - yesterdayVisitors) / yesterdayVisitors) * 100;
} else if (dailyVisitors > 0) {
trend = 100;
}

const stats = {
totalUsers,
totalPlaces,
totalAds,
visitors: totalVisits,
onlineMembers,
dailyVisitors,
trend: Math.round(trend),
weeklyTraffic
};

res.json(stats);
} catch (error) {
console.error('Error fetching dashboard stats:', error);
res.status(500).json({ message: 'Error fetching stats' });
}
};

const getSystemContainers = async (req, res) => {
try {
const containers = await dockerService.listContainers();
res.json(containers);
} catch (error) {
console.error('Error fetching system containers:', error);
res.status(500).json({ message: 'Failed to fetch containers info', error: error.message });
}
};

const getContainerLogs = async (req, res) => {
try {
const { id } = req.params;
const { tail } = req.query;
const logs = await dockerService.getContainerLogs(id, tail ? parseInt(tail) : 200);
res.json({ logs });
} catch (error) {
res.status(500).json({ message: 'Failed to fetch logs', error: error.message });
}
};

const restartSystemContainer = async (req, res) => {
try {
const { id } = req.params;
await dockerService.restartContainer(id);
res.json({ message: 'Container restarting initiated' });
} catch (error) {
res.status(500).json({ message: 'Failed to restart container', error: error.message });
}
};

const startSystemContainer = async (req, res) => {
try {
const { id } = req.params;
await dockerService.startContainer(id);
res.json({ message: 'Container started successfully' });
} catch (error) {
res.status(500).json({ message: 'Failed to start container', error: error.message });
}
};

const stopSystemContainer = async (req, res) => {
try {
const { id } = req.params;
await dockerService.stopContainer(id);
res.json({ message: 'Container stopped successfully' });
} catch (error) {
res.status(500).json({ message: 'Failed to stop container', error: error.message });
}
};

module.exports = {
getDashboardStats,
getSystemContainers,
getContainerLogs,
restartSystemContainer,
startSystemContainer,
stopSystemContainer
};
76 changes: 76 additions & 0 deletions backend/src/controllers/alerts.controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
const SysAlert = require('../models/SysAlert');
const { Op } = require('sequelize');
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused variable Op.

Suggested change
const { Op } = require('sequelize');

Copilot uses AI. Check for mistakes.

exports.getAlerts = async (req, res) => {
try {
const { resolved } = req.query;
const whereClause = {};

if (resolved !== undefined) {
whereClause.isResolved = resolved === 'true';
}

const alerts = await SysAlert.findAll({
where: whereClause,
order: [['createdAt', 'DESC']],
limit: 100 // Prevent fetching too many logs
});
res.status(200).json(alerts);
} catch (error) {
console.error('Error fetching alerts:', error);
res.status(500).json({ message: 'Failed to fetch alerts', error: error.message });
}
};

exports.createAlert = async (req, res) => {
try {
const { type, message, details, source } = req.body;

const alert = await SysAlert.create({
type: type || 'info',
message,
details: typeof details === 'object' ? JSON.stringify(details, null, 2) : details,
source: source || 'admin-api'
});

res.status(201).json(alert);
} catch (error) {
console.error('Error creating alert:', error);
res.status(500).json({ message: 'Failed to create alert', error: error.message });
}
};

exports.resolveAlert = async (req, res) => {
try {
const { id } = req.params;
const alert = await SysAlert.findByPk(id);

if (!alert) {
return res.status(404).json({ message: 'Alert not found' });
}

alert.isResolved = true;
await alert.save();

res.status(200).json({ message: 'Alert resolved', alert });
} catch (error) {
console.error('Error resolving alert:', error);
res.status(500).json({ message: 'Failed to resolve alert', error: error.message });
}
};

exports.deleteAlert = async (req, res) => {
try {
const { id } = req.params;
const result = await SysAlert.destroy({ where: { id } });

if (!result) {
return res.status(404).json({ message: 'Alert not found' });
}

res.status(200).json({ message: 'Alert deleted' });
} catch (error) {
console.error('Error deleting alert:', error);
res.status(500).json({ message: 'Failed to delete alert', error: error.message });
}
};
Loading
Loading