user managment

This commit is contained in:
Cyril Rouillon 2024-06-24 18:37:04 +02:00
parent bafef0f6b3
commit 0c7fc6fdf7
50 changed files with 2748 additions and 219 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
backend/node_modules
backend/.Keys.js
client/.angular
client/node_modules

23
backend/config.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = {
db: {
connectionString: "mongodb://db:27017",
dbName: "jucundus",
},
session: {
sessionCollection: "Session",
sessionConfig: {
name: "jucundus.sid",
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 30, // 30 days
httpOnly: true, // only accessible by the server
secure: true,
},
resave: false, // don't save session if unmodified
saveUninitialized: true,
}
},
jwtOptions: {
issuer: "jucundus.com",
audience: "yoursite",
}
};

View File

@ -38,7 +38,6 @@ exports.getPictures = asyncHandler(async (req, res, next) => {
});
});
exports.getLotsBySale = asyncHandler(async (req, res, next) => {
let id = req.params.id
@ -165,3 +164,67 @@ exports.AuctionedItem = asyncHandler(async (req, res, next) => {
}
});
// DB
exports.get = asyncHandler(async (req, res, next) => {
try{
const id = req.params.id;
let result = await lotDb.get(id);
res.status(200).send(result);
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.post = asyncHandler(async (req, res, next) => {
try{
// check if double
let Lot = await lotDb.getByIDPlatform(req.body.idPlatform, req.body.platform);
if(Sale){
return res.status(500).send("Lot already exists");
}
let createLot = await lotDb.post(req.body);
res.status(204).send();
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.put = asyncHandler(async (req, res, next) => {
try{
const id = req.params.id;
let updatedDocument = { ...req.body };
delete updatedDocument._id;
console.log(updatedDocument);
let result = await lotDb.put(id, updatedDocument);
console.log(result);
res.status(200).send(result);
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.delete = asyncHandler(async (req, res, next) => {
try{
const id = req.params.id;
// Remove the lot
await lotDb.remove(id);
res.status(200).send({"message": "Lots deleted"});
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});

View File

@ -8,7 +8,7 @@ const lotDb = new LotDb();
const agenda = require('../services/agenda');
const {Agent} = require('../services/agent');
const agent = new Agent();
const ExcelJS = require('exceljs');
exports.getSaleInfos = asyncHandler(async (req, res, next) => {
let url = req.params.url
@ -218,31 +218,102 @@ exports.postProcessing = asyncHandler(async (req, res, next) => {
}
Lots = await lotDb.getBySaleId(Sale._id.toString(),Sale.platform);
TimestampInSecond = (timestamp) => {
const stringTimestamp = String(timestamp);
if (stringTimestamp.length === 13) {
return timestamp / 1000;
} else if (stringTimestamp.length === 10) {
return timestamp;
} else {
return 0;
}
}
// Create an array to hold the updated lots
let updatedLots = [];
let bidsDuration = 0;
// process each lot
for (let lot of Lots) {
let highestBid, duration, percentageAboveUnderLow, percentageAboveUnderHigh = 0;
// if bid
let nbrBids = 0;
if (Array.isArray(lot.Bids)) {
nbrBids = lot.Bids.length;
highestBid = lot.Bids.reduce((prev, current) => (prev.amount > current.amount) ? prev : current).amount;
let startTime = TimestampInSecond(lot.Bids[0].timestamp);
let endTime = TimestampInSecond(lot.Bids[lot.Bids.length-1].timestamp);
duration = endTime - startTime;
// total time of bids
bidsDuration += duration;
duration = duration.toFixed(0);
}
// if auctioned
percentageAboveUnderLow = 0;
percentageAboveUnderHigh = 0;
if (lot.auctioned) {
if(lot.EstimateLow){
percentageAboveUnderLow = ((lot.auctioned.amount - lot.EstimateLow) / lot.EstimateLow) * 100;
}
if(lot.EstimateHigh){
percentageAboveUnderHigh = ((lot.auctioned.amount - lot.EstimateHigh) / lot.EstimateHigh) * 100;
}
}
let lotPostProcessing = {
nbrBids: nbrBids,
highestBid: highestBid,
duration: duration,
percentageAboveUnderLow: percentageAboveUnderLow.toFixed(0),
percentageAboveUnderHigh: percentageAboveUnderHigh.toFixed(0)
}
lot.postProcessing = lotPostProcessing;
await lotDb.put(lot._id, lot);
// Add the updated lot to the array
updatedLots.push(lot);
}
// refresh with postprocess datas
Lots = updatedLots;
let startTime = 0;
if (Array.isArray(Lots[0].Bids)) {
startTime = Lots[0].Bids[0].timestamp;
startTime = TimestampInSecond(Lots[0].Bids[0].timestamp);
}else{
startTime = Lots[0].timestamp;
startTime = TimestampInSecond(Lots[0].timestamp);
}
let LastBid = [...Lots].reverse().find(lot => lot.auctioned !== undefined);
let endTime = 0;
if (Array.isArray(LastBid.Bids)) {
endTime = LastBid.Bids[LastBid.Bids.length-1].timestamp;
endTime = TimestampInSecond(LastBid.Bids[LastBid.Bids.length-1].timestamp);
}else{
endTime = LastBid.timestamp;
endTime = TimestampInSecond(LastBid.timestamp);
}
console.log("Start Time: "+startTime);
console.log("End Time: "+endTime);
let duration = endTime-startTime;
let duration = (endTime-startTime).toFixed(0);
let totalAmount = 0;
let unsoldLots = 0;
for (let lot of Lots) {
if (lot.auctioned) {
totalAmount += lot.auctioned.amount;
} else {
unsoldLots++;
}
}
@ -262,10 +333,15 @@ exports.postProcessing = asyncHandler(async (req, res, next) => {
let postProcessing = {
nbrLots: Lots.length,
duration: duration,
bidsDuration: bidsDuration.toFixed(0),
durationPerLots: (duration/Lots.length).toFixed(0),
totalAmount: totalAmount,
averageAmount: (totalAmount/Lots.length).toFixed(2),
medianAmount: calculateMedian(amounts).toFixed(2),
minAmount: Math.min(...amounts).toFixed(2),
maxAmount: Math.max(...amounts).toFixed(2),
unsoldLots: unsoldLots,
unsoldPercentage: ((unsoldLots/Lots.length)*100).toFixed(2)
}
console.log(postProcessing);
@ -279,4 +355,86 @@ exports.postProcessing = asyncHandler(async (req, res, next) => {
return res.status(500).send(err);
}
});
exports.SaleStatXsl = asyncHandler(async (req, res, next) => {
try{
const id = req.params.id;
Sale = await saleDb.get(id);
if(!Sale){
console.error("Sale not found");
return res.status(404).send("Sale not found");
}
Lots = await lotDb.getBySaleId(Sale._id.toString(),Sale.platform);
const workbook = new ExcelJS.Workbook();
const worksheet = workbook.addWorksheet('Sale Stats');
worksheet.columns = [
{ header: 'Lot #', key: 'lotNumber', width: 10 },
{ header: 'Description', key: 'description', width: 100 },
{ header: 'Auctioned Amount', key: 'auctionedAmount', width: 20 },
{ header: 'Estimate Low', key: 'estimateLow', width: 20 },
{ header: 'Estimate High', key: 'estimateHigh', width: 20 },
{ header: 'Bids', key: 'nbrBids', width: 10 },
{ header: 'Highest Bid', key: 'highestBid', width: 20 },
{ header: 'Duration (in s)', key: 'duration', width: 10 },
{ header: '% Above Low', key: 'percentageAboveUnderLow', width: 20 },
{ header: '% Above High', key: 'percentageAboveUnderHigh', width: 20 }
];
let row = 2;
for (let lot of Lots) {
let Row = worksheet.addRow({
lotNumber: lot.lotNumber,
description: lot.description,
auctionedAmount: lot.auctioned?.amount,
estimateLow: lot.EstimateLow,
estimateHigh: lot.EstimateHigh,
nbrBids: lot.postProcessing?.nbrBids,
highestBid: lot.postProcessing?.highestBid,
duration: lot.postProcessing?.duration,
percentageAboveUnderLow: lot.postProcessing?.percentageAboveUnderLow/100,
percentageAboveUnderHigh: lot.postProcessing?.percentageAboveUnderHigh/100
});
Row.getCell('C').numFmt = '€0.00';
Row.getCell('D').numFmt = '€0.00';
Row.getCell('E').numFmt = '€0.00';
Row.getCell('B').numFmt = '€0.00';
Row.getCell('G').numFmt = '€0.00';
Row.getCell('I').numFmt = '0%';
Row.getCell('J').numFmt = '0%';
row++;
}
worksheet.addRow({
lotNumber: 'Total',
auctionedAmount: Sale.postProcessing?.totalAmount,
estimateLow: '',
estimateHigh: '',
nbrBids: '',
highestBid: '',
duration: Sale.postProcessing?.duration,
percentageAboveUnderLow: '',
percentageAboveUnderHigh: ''
});
// send the Xls File
res.setHeader(
"Content-Disposition",
"attachment; filename=" + "SaleStats.xlsx"
);
await workbook.xlsx.write(res);
return res.status(200).end();
}catch(err){
console.log(err);
return res.status(500).send
}
});

199
backend/controllers/user.js Normal file
View File

@ -0,0 +1,199 @@
const asyncHandler = require("express-async-handler");
const moment = require('moment-timezone');
const { ObjectId } = require('mongodb');
const { UserDb } = require("../services/userDb");
const crypto = require('crypto');
function ClearUserData(user){
delete user.salt;
delete user.hashed_password;
delete user.salt;
delete user.isAgent;
return user;
}
function ClearUserDataForAdmin(user){
delete user.salt;
delete user.hashed_password;
delete user.salt;
return user;
}
// DB
exports.get = asyncHandler(async (req, res, next) => {
try{
const userDb = await UserDb.init();
const id = req.params.id;
let result = await userDb.get(id);
if (req.user.isAdmin){
res.status(200).send(ClearUserDataForAdmin(result));
}else{
res.status(200).send(ClearUserData(result));
}
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.post = asyncHandler(async (req, res, next) => {
try{
const userDb = await UserDb.init();
// check if double
let User = await userDb.getByEmail(req.body.email);
if(User){
return res.status(500).send("User already exists");
}
// check password
if(!req.body.password){
return res.status(500).send("Password not set");
}
if(req.body.password != req.body.confirmPassword){
return res.status(500).send("Passwords do not match");
}
if(req.body.password.length < 8){
return res.status(500).send("Password too short");
}
if(req.body.isAdmin){
if(req.user){
if(!req.user.isAdmin){
return res.status(500).send("You are not allowed to create an admin user");
}
}else{
req.body.isAdmin = false
}
}
if(req.body.isAgent){
if(req.user){
if(!req.user.isAgent){
return res.status(500).send("You are not allowed to create an agent user");
}
}else{
req.body.isAgent = false
}
}
let salt = crypto.randomBytes(16).toString('hex');
let user = {
username: req.body.username,
hashed_password: crypto.pbkdf2Sync(req.body.password, salt, 310000, 32, 'sha256').toString('hex'),
salt: salt,
email: req.body.email,
isAdmin: req.body.isAdmin,
isAgent: req.body.isAgent,
}
let createData = await userDb.post(user);
res.status(204).send();
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.put = asyncHandler(async (req, res, next) => {
try{
const userDb = await UserDb.init();
const id = req.params.id;
const User = await userDb.get(id);
if(!User){
return res.status(500).send("User not found");
}
// check password
let hashed_password = "";
let salt = "";
if(req.body.password){
if(req.body.password != req.body.confirmPassword){
return res.status(500).send("Passwords do not match");
}
if(req.body.password.length < 8){
return res.status(500).send("Password too short");
}
salt = crypto.randomBytes(16).toString('hex');
hashed_password = crypto.pbkdf2Sync(req.body.password, salt, 310000, 32, 'sha256').toString('hex');
}else{
salt = User.salt;
hashed_password = User.hashed_password;
}
if(req.body.isAdmin){
if(!req.user.isAdmin){
return res.status(500).send("You are not allowed to create an admin user");
}
}
if(req.body.isAgent){
if(!req.user.isAdmin){
return res.status(500).send("You are not allowed to create an agent user");
}
}
let user = {
username: req.body.username,
hashed_password: hashed_password,
salt: salt,
email: req.body.email,
isAdmin: req.body.isAdmin,
isAgent: req.body.isAgent,
}
let result = await userDb.put(id, user);
console.log(result);
res.status(200).send(result);
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.delete = asyncHandler(async (req, res, next) => {
try{
const userDb = await UserDb.init();
const id = req.params.id;
// Remove the sale
await userDb.remove(id);
res.status(200).send({"message": "User deleted"});
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
// Functions
exports.current = asyncHandler(async (req, res, next) => {
try{
const user = ClearUserData(req.user);
res.status(200).send(user);
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});
exports.getAllUsers = asyncHandler(async (req, res, next) => {
try{
const userDb = await UserDb.init();
let result = await userDb.getAll();
result = result.map(user => ClearUserDataForAdmin(user));
res.status(200).send(result);
}catch(err){
console.log(err);
return res.status(500).send(err);
}
});

View File

@ -1,3 +1,4 @@
const config = require("./config.js");
const express = require('express')
const app = express()
@ -9,17 +10,60 @@ app.use(cors());
// Enable preflight requests for all routes
app.options('*', cors());
// Session support
const session = require('express-session');
app.use(session({
secret: 'jucundus.ses',
resave: false,
saveUninitialized: true,
cookie: { secure: true }
}))
// const MongoStore = require('connect-mongo');
// const keys = require("./.Keys.js");
// const sassionConfig = {
// ...config.session.sessionConfig,
// secret: keys.session,
// store: MongoStore.create({
// mongoUrl: `${config.db.connectionString}/${config.db.dbName}`,
// collection: config.session.sessionCollection,
// stringify: false,
// autoReconnect: true,
// autoRemove: 'native'
// })};
// app.use(session(sassionConfig));
// Authentication
const passport = require('passport');
app.use(passport.initialize());
app.use(passport.session());
app.use('/', require('./routes/auth'));
// Agenda Scheduller
const agenda = require('./services/agenda');
(async function() {
//lunch sheduller
await agenda.start();
})();
//create first user
const { UserDb } = require('./services/userDb');
const userDb = await UserDb.init();
userDb.creatFirstUserifEmpty();
})();
// Agenda UI
var Agendash = require("agendash");
app.use("/dash", Agendash(agenda));
// routes
app.use('/api/user', require('./routes/user'));
app.use('/api/lot', require('./routes/lot'));
app.use('/api/sale', require('./routes/sale'));
app.use('/api/favorite', require('./routes/favorite'));

View File

@ -0,0 +1,33 @@
function checkIsConcernedUserOrAdmin(req, res, next) {
const user = req.user; // User is set by Passport
const userIdParam = req.params.id;
if (user.isAdmin === true || user._id === userIdParam) {
next();
} else {
res.status(403).json({ error: 'Forbidden' });
}
}
function checkIsAdmin(req, res, next) {
const user = req.user; // User is set by Passport
if (user.isAdmin === true) {
next();
} else {
res.status(403).json({ error: 'Forbidden' });
}
}
function checkIsAgent(req, res, next) {
const user = req.user; // User is set by Passport
if (user.isAgent === true) {
next();
} else {
res.status(403).json({ error: 'Forbidden' });
}
}
module.exports = { checkIsConcernedUserOrAdmin, checkIsAgent, checkIsAdmin };

1141
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon --watch ./ server.js --ignore node_modules/"
"dev": "nodemon --inspect=0.0.0.0 --watch ./ server.js --ignore node_modules/"
},
"keywords": [],
"author": "",
@ -15,11 +15,18 @@
"@angular/cli": "^17.1.3",
"@hokify/agenda": "^6.3.0",
"agendash": "^4.0.0",
"connect-mongo": "^5.1.0",
"cors": "^2.8.5",
"exceljs": "^4.4.0",
"express": "^4.18.2",
"express-async-handler": "^1.2.0",
"express-session": "^1.18.0",
"jsonwebtoken": "^9.0.2",
"mongodb": "^6.5.0",
"node-fetch": "^2.7.0"
"node-fetch": "^2.7.0",
"passport": "^0.7.0",
"passport-jwt": "^4.0.1",
"passport-local": "^1.0.0"
},
"devDependencies": {
"nodemon": "^3.0.3"

113
backend/routes/auth.js Normal file
View File

@ -0,0 +1,113 @@
var passport = require('passport');
var LocalStrategy = require('passport-local');
var crypto = require('crypto');
const router = require('express').Router()
const config = require("../config.js");
const keys = require("../.Keys.js");
/* Configure password authentication strategy.
*
* The `LocalStrategy` authenticates users by verifying a username and password.
* The strategy parses the username and password from the request and calls the
* `verify` function.
*
* The `verify` function queries the database for the user record and verifies
* the password by hashing the password supplied by the user and comparing it to
* the hashed password stored in the database. If the comparison succeeds, the
* user is authenticated; otherwise, not.
*/
const { UserDb }= require('../services/userDb');
passport.use(new LocalStrategy({
usernameField: 'email',
passwordField: 'password'
},async function verify(email, password, cb) {
try {
const userDb = await UserDb.init();
const user = await userDb.getByEmail(email);
if (!user) {
return cb(null, false, { message: 'Incorrect username or password.' });
}
crypto.pbkdf2(password, user.salt, 310000, 32, 'sha256', async function(err, hashedPassword) {
if (err) { return cb(err); }
if (!crypto.timingSafeEqual(Buffer.from(user.hashed_password, 'hex'), Buffer.from(hashedPassword, 'hex'))) {
return cb(null, false, { message: 'Incorrect username or password.' });
}
return cb(null, user);
});
} catch (err) {
return cb(err);
}
}));
/* Configure session management.
*
* When a login session is established, information about the user will be
* stored in the session. This information is supplied by the `serializeUser`
* function, which is yielding the user ID and username.
*
* As the user interacts with the app, subsequent requests will be authenticated
* by verifying the session. The same user information that was serialized at
* session establishment will be restored when the session is authenticated by
* the `deserializeUser` function.
*
* Since every request to the app needs the user ID and username, in order to
* fetch todo records and render the user element in the navigation bar, that
* information is stored in the session.
*/
passport.serializeUser(function(user, cb) {
process.nextTick(function() {
cb(null, { id: user._id, username: user.email });
});
});
passport.deserializeUser(function(user, cb) {
process.nextTick(function() {
return cb(null, user);
});
});
// JWT
var jwt = require('jsonwebtoken');
var JwtStrategy = require('passport-jwt').Strategy,
ExtractJwt = require('passport-jwt').ExtractJwt;
var opts = {}
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = keys.jwt;
opts.issuer = config.jwtOptions.issuer;
opts.audience = config.jwtOptions.audience;
passport.use(new JwtStrategy(opts, async function(jwt_payload, done) {
const userDb = await UserDb.init();
try {
const user = await userDb.get(jwt_payload.sub);
if (user) {
return done(null, user);
} else {
return done(null, false);
// or you could create a new account
}
} catch (err) {
return done(err, false);
}
}));
router.post('/authenticate', async function(req, res) {
passport.authenticate('local', async function(err, user, info) {
if (err) { return res.status(500).json({message: err.message}); }
if (!user) { return res.status(401).json({message: 'Incorrect email or password.'}); }
// User found, generate a JWT for the user
var token = jwt.sign({ sub: user._id, email: user.email }, opts.secretOrKey, {
issuer: opts.issuer,
audience: opts.audience,
expiresIn: 86400 * 30 // 30 days
});
res.json({ token: token });
//return res.status(500).json({message: 'Incorrect email or password.'})
})(req, res);
});
module.exports = router;

View File

@ -1,7 +1,9 @@
const controllers = require('../controllers/favorite')
const router = require('express').Router()
const passport = require('passport');
const { checkIsConcernedUserOrAdmin } = require('../middleware/authMiddleware')
router.post('/save/', controllers.save)
router.get('/getAll/', controllers.getAll)
router.post('/save/', passport.authenticate('jwt', { session: false }), controllers.save)
router.get('/getAll/', passport.authenticate('jwt', { session: false }), controllers.getAll)
module.exports = router

View File

@ -1,12 +1,21 @@
const controllers = require('../controllers/lot')
const router = require('express').Router()
const passport = require('passport');
const { checkIsAgent, checkIsAdmin } = require('../middleware/authMiddleware')
router.get('/getInfos/:url', controllers.getInfos)
router.get('/getPictures/:url', controllers.getPictures)
router.get('/getLotsBySale/:id', controllers.getLotsBySale)
router.get('/getInfos/:url', passport.authenticate('jwt', { session: false }), controllers.getInfos)
router.get('/getPictures/:url', passport.authenticate('jwt', { session: false }), controllers.getPictures)
router.get('/getLotsBySale/:id', passport.authenticate('jwt', { session: false }), controllers.getLotsBySale)
router.post('/NextItem/', controllers.NextItem)
router.post('/AuctionedItem/', controllers.AuctionedItem)
router.post('/Bid/', controllers.Bid)
// DB
router.get('/lot/:id', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.get)
router.post('/lot/', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.post)
router.put('/lot/:id', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.put)
router.delete('/lot/:id', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.delete)
// Live Data
router.post('/NextItem/', checkIsAgent, controllers.NextItem)
router.post('/AuctionedItem/', checkIsAgent, controllers.AuctionedItem)
router.post('/Bid/', checkIsAgent, controllers.Bid)
module.exports = router

View File

@ -1,4 +1,5 @@
const controllers = require('../controllers/sale')
const passport = require('passport');
const router = require('express').Router()
// AuctionAgent
@ -13,9 +14,11 @@ router.post('/sale/', controllers.post)
router.put('/sale/:id', controllers.put)
router.delete('/sale/:id', controllers.delete)
router.get('/getAll/', controllers.getAll)
//router.get('/getAll/', controllers.getAll)
router.get('/getAll/', passport.authenticate('jwt', { session: false }), controllers.getAll);
router.get('/getByUrl/:url', controllers.getByUrl)
router.get('/postProcessing/:id', controllers.postProcessing)
router.get('/SaleStatXsl/:id', controllers.SaleStatXsl)
module.exports = router

15
backend/routes/user.js Normal file
View File

@ -0,0 +1,15 @@
const controllers = require('../controllers/user')
const passport = require('passport');
const router = require('express').Router()
const { checkIsConcernedUserOrAdmin, checkIsAdmin } = require('../middleware/authMiddleware')
// DB
router.get('/user/:id', passport.authenticate('jwt', { session: false }), checkIsConcernedUserOrAdmin, controllers.get);
router.post('/user/', controllers.post)
router.put('/user/:id', passport.authenticate('jwt', { session: false }), checkIsConcernedUserOrAdmin, controllers.put)
router.delete('/user/:id', passport.authenticate('jwt', { session: false }), checkIsConcernedUserOrAdmin, controllers.delete)
router.get('/current', passport.authenticate('jwt', { session: false }), controllers.current)
router.get('/getAllUsers', passport.authenticate('jwt', { session: false }), checkIsAdmin, controllers.getAllUsers)
module.exports = router

View File

@ -1,5 +1,6 @@
const MongoClient = require("mongodb").MongoClient;
const connectionString = "mongodb://db:27017";
const config = require("../config.js");
const connectionString = config.db.connectionString;
const client = new MongoClient(connectionString);
let db;
@ -8,7 +9,7 @@ const connectDb = async () => {
if (db) return db;
try {
const conn = await client.connect();
db = conn.db("jucundus");
db = conn.db(config.db.dbName);
return db;
} catch(e) {
console.error(e);

View File

@ -0,0 +1,91 @@
const { ObjectId } = require('mongodb');
const connectDb = require("./db");
const crypto = require('crypto');
const UserDb = class
{
constructor()
{
}
static async init() {
const userDb = new UserDb();
await userDb.getCollection();
return userDb;
}
async getCollection()
{
const db = await connectDb();
if (!db) {
throw new Error('Database not connected');
}
this.collection = db.collection("Users");
}
// CRUD
async get(id)
{
let result = await this.collection.findOne({_id: new ObjectId(id)});
return result;
}
async post(newDocument)
{
delete newDocument._id;
let result = await this.collection.insertOne(newDocument);
return result;
}
async put(id, data)
{
delete data._id;
let result = await this.collection.updateOne({_id: new ObjectId(id)}, {$set: data});
return result;
}
async remove(id)
{
let result = await this.collection.deleteOne({_id: new ObjectId(id)});
return result;
}
// Functions
async getAll()
{
let result = await this.collection.find({}).toArray();
return result;
}
async getByUsername(username)
{
let result = await this.collection.findOne({username: username});
return result;
}
async getByEmail(email)
{
let result = await this.collection.findOne({email: email});
return result;
}
async creatFirstUserifEmpty( ){
const allUsers = await this.getAll();
if(allUsers.length == 0){
console.log("Creating first user");
let salt = crypto.randomBytes(16).toString('hex');
let user = {
username: "admin",
hashed_password: crypto.pbkdf2Sync('admin', salt, 310000, 32, 'sha256').toString('hex'),
salt: salt,
email: "admin@admin.com",
isAdmin: true,
isAgent: false,
}
this.post(user);
}
}
}
module.exports = { UserDb };

View File

@ -0,0 +1,19 @@
import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../../../environments/environment';
@Injectable({
providedIn: 'root'
})
export class apiAuthService {
ServeurURL = environment.ServeurURL;
ApiURL = this.ServeurURL+"/api";
constructor(private http: HttpClient){}
// authenticate
authenticate(email: string, password: string) {
return this.http.post<{token: string}>(this.ServeurURL+'/authenticate', {email, password});
}
}

View File

@ -0,0 +1,26 @@
import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../../../environments/environment';
import { LotInfo } from '../model/lotInfo.interface';
import { SaleInfo } from '../model/saleInfo.interface';
@Injectable({
providedIn: 'root'
})
export class apiFavoriteService {
ServeurURL = environment.ServeurURL;
ApiURL = this.ServeurURL+"/api";
constructor(private http: HttpClient){}
//Favorites
saveFavorite(lotInfo: LotInfo, saleInfo: SaleInfo, picture: String, dateTime: string, buyProject: boolean, maxPrice: number, Note: string): Observable<any> {
return this.http.post(this.ApiURL+'/favorite/save', {lotInfo, saleInfo, picture, dateTime, buyProject, maxPrice, Note});
}
getAllFavorite(): Observable<any> {
return this.http.get(this.ApiURL+'/favorite/getAll');
}
}

View File

@ -0,0 +1,49 @@
import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../../../environments/environment';
import { LotInfo } from '../model/lotInfo.interface';
import { Lot } from '../model/lot.interface';
@Injectable({
providedIn: 'root'
})
export class apiLotService {
ServeurURL = environment.ServeurURL;
ApiURL = this.ServeurURL+"/api";
constructor(private http: HttpClient){}
// Lot
getLotInfo(url: string): Observable<LotInfo> {
let encodeUrl = encodeURIComponent(url);
return this.http.get<LotInfo>(this.ApiURL+'/lot/getInfos/'+encodeUrl);
}
getPictures(url: string): Observable<String[]> {
let encodeUrl = encodeURIComponent(url);
return this.http.get<String[]>(this.ApiURL+'/lot/getPictures/'+encodeUrl);
}
getLotsBySale(_id: String): Observable<Lot[]> {
return this.http.get<Lot[]>(this.ApiURL+'/lot/getLotsBySale/'+_id);
}
// Lot CRUD
getLot(_id: String): Observable<Lot> {
return this.http.get<Lot>(this.ApiURL+'/lot/lot/'+_id);
}
saveLot(Lot: Lot): Observable<Lot> {
return this.http.post<Lot>(this.ApiURL+'/lot/lot', Lot);
}
updateLot(Lot: Lot): Observable<Lot> {
return this.http.put<Lot>(this.ApiURL+'/lot/lot/'+Lot._id, Lot);
}
deleteLot(_id: String): Observable<any> {
return this.http.delete<any>(this.ApiURL+'/lot/lot/'+_id);
}
}

View File

@ -1,34 +1,20 @@
import { Injectable, Inject } from '@angular/core';
import { Injectable} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../../../environments/environment';
import { LotInfo } from './model/lotInfo.interface';
import { SaleInfo } from './model/saleInfo.interface';
import { Sale } from './model/sale.interface';
import { Lot } from './model/lot.interface';
import { SaleInfo } from '../model/saleInfo.interface';
import { Sale } from '../model/sale.interface';
@Injectable({
providedIn: 'root'
})
export class apiService {
ApiURL = "http://localhost:3000/api";
export class apiSaleService {
ServeurURL = environment.ServeurURL;
ApiURL = this.ServeurURL+"/api";
constructor(private http: HttpClient){}
// Lot
getLotInfo(url: string): Observable<LotInfo> {
let encodeUrl = encodeURIComponent(url);
return this.http.get<LotInfo>(this.ApiURL+'/lot/getInfos/'+encodeUrl);
}
getPictures(url: string): Observable<String[]> {
let encodeUrl = encodeURIComponent(url);
return this.http.get<String[]>(this.ApiURL+'/lot/getPictures/'+encodeUrl);
}
getLotsBySale(_id: String): Observable<Lot[]> {
return this.http.get<Lot[]>(this.ApiURL+'/lot/getLotsBySale/'+_id);
}
constructor(
private http: HttpClient){}
// Sale
getSaleInfos(url: string): Observable<SaleInfo> {
@ -74,24 +60,18 @@ export class apiService {
return this.http.delete<any>(this.ApiURL+'/sale/sale/'+_id);
}
// Function DB Sale
// Function DB Sale
getAllSale(): Observable<SaleInfo[]> {
return this.http.get<SaleInfo[]>(this.ApiURL+'/sale/getAll');
return this.http.get<SaleInfo[]>(`${this.ApiURL}/sale/getAll`);
}
postProcessing(_id: String): Observable<any> {
return this.http.get<any>(this.ApiURL+'/sale/postProcessing/'+_id);
}
getSaleStatXsl(_id: String): Observable<any> {
return this.http.get<Blob>(this.ApiURL+'/sale/SaleStatXsl/'+_id, { responseType: 'blob' as 'json' });
}
//Favorites
saveFavorite(lotInfo: LotInfo, saleInfo: SaleInfo, picture: String, dateTime: string, buyProject: boolean, maxPrice: number, Note: string): Observable<any> {
return this.http.post(this.ApiURL+'/favorite/save', {lotInfo, saleInfo, picture, dateTime, buyProject, maxPrice, Note});
}
getAllFavorite(): Observable<any> {
return this.http.get(this.ApiURL+'/favorite/getAll');
}
}

View File

@ -0,0 +1,43 @@
import { Injectable} from '@angular/core';
import { HttpClient} from '@angular/common/http';
import { Observable } from 'rxjs';
import { environment } from '../../../../environments/environment';
import { User } from '../model/user.interface';
@Injectable({
providedIn: 'root'
})
export class apiUserService {
ServeurURL = environment.ServeurURL;
ApiURL = this.ServeurURL+"/api";
constructor(
private http: HttpClient){}
// User CRUD
getUser(_id: String): Observable<User> {
return this.http.get<User>(this.ApiURL+'/user/user/'+_id);
}
saveUser(User: User): Observable<User> {
return this.http.post<User>(this.ApiURL+'/user/user/', User);
}
updateUser(User: User): Observable<User> {
return this.http.put<User>(this.ApiURL+'/user/user/'+User._id, User);
}
deleteUser(_id: String): Observable<any> {
return this.http.delete<any>(this.ApiURL+'/user/user/'+_id);
}
// User function
getCurrentUser(): Observable<User> {
return this.http.get<User>(this.ApiURL+'/user/current');
}
getAllUsers(): Observable<User[]> {
return this.http.get<User[]>(this.ApiURL+'/user/getAllUsers');
}
}

View File

@ -1,10 +1,11 @@
import { Injectable, Inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { delay, map } from 'rxjs/operators';
import * as jwt_decode from 'jwt-decode';
import { apiAuthService } from './api/api.auth.service';
import { apiUserService } from './api/api.user.service';
import { User } from './model/user.interface';
import * as moment from 'moment';
import { environment } from '../../../environments/environment';
import { of, EMPTY } from 'rxjs';
@Injectable({
@ -12,30 +13,40 @@ import { of, EMPTY } from 'rxjs';
})
export class AuthenticationService {
constructor(private http: HttpClient,
constructor(
private apiAuthService: apiAuthService,
private apiUserService: apiUserService,
@Inject('LOCALSTORAGE') private localStorage: Storage) {
}
login(email: string, password: string) {
return of(true)
.pipe(delay(1000),
map((/*response*/) => {
// set token property
// const decodedToken = jwt_decode(response['token']);
// store email and jwt token in local storage to keep user logged in between page refreshes
this.localStorage.setItem('currentUser', JSON.stringify({
token: 'aisdnaksjdn,axmnczm',
isAdmin: true,
email: 'john.doe@gmail.com',
id: '12312323232',
alias: 'john.doe@gmail.com'.split('@')[0],
expiration: moment().add(1, 'days').toDate(),
fullName: 'John Doe'
}));
return true;
return this.apiAuthService.authenticate(email, password).
pipe(map(response => {
// If the server returns a token, the login is successful
if (response.token) {
// Store user details and jwt token in local storage to keep user logged in between page refreshes
this.localStorage.setItem('currentUser', JSON.stringify({
token: response.token,
}));
this.apiUserService.getCurrentUser().subscribe((user: User) => {
console.log(user);
const Storeduser = this.localStorage.getItem('currentUser');
if(Storeduser) {
const currentUser = JSON.parse(Storeduser);
currentUser.isAdmin = user.isAdmin;
currentUser.email = user.email;
currentUser.id = user._id;
currentUser.alias = user.email.split('@')[0];
currentUser.fullName = user.username;
currentUser.expiration = moment().add(30, 'days').toDate();
this.localStorage.setItem('currentUser', JSON.stringify(currentUser));
}
});
return true;
}
return false;
}));
}
logout(): void {
@ -45,16 +56,20 @@ export class AuthenticationService {
getCurrentUser(): any {
// TODO: Enable after implementation
// return JSON.parse(this.localStorage.getItem('currentUser'));
return {
token: 'aisdnaksjdn,axmnczm',
isAdmin: true,
email: 'john.doe@gmail.com',
id: '12312323232',
alias: 'john.doe@gmail.com'.split('@')[0],
expiration: moment().add(1, 'days').toDate(),
fullName: 'John Doe'
};
const user = this.localStorage.getItem('currentUser');
if (!user) {
return null;
}
return JSON.parse(user);
// return {
// token: JSON.parse(user).token,
// isAdmin: true,
// email: 'john.doe@gmail.com',
// id: '12312323232',
// alias: 'john.doe@gmail.com'.split('@')[0],
// expiration: moment().add(1, 'days').toDate(),
// fullName: 'John Doe'
// };
}
passwordResetRequest(email: string) {

View File

@ -2,7 +2,7 @@
export interface Bid {
timestamp: string;
amount: number;
auctioned_type: string;
auctioned_type?: string;
}
export interface Auctioned {
@ -12,6 +12,14 @@ export interface Auctioned {
sold: boolean;
}
export interface PostProcessing {
nbrBids: number;
highestBid: number;
duration: number;
percentageAboveUnderLow: number;
percentageAboveUnderHigh: number;
}
export interface Lot {
_id: {
$oid: string;
@ -20,10 +28,15 @@ export interface Lot {
platform: string;
timestamp: string;
lotNumber: string;
title?: string;
description?: string;
EstimateLow?: number;
EstimateHigh?: number;
RawData?: Object;
sale_id: {
$oid: string;
};
Bids?: Bid[];
auctioned?: Auctioned;
postProcessing?: PostProcessing;
}

View File

@ -2,10 +2,15 @@
export interface PostProcessing {
nbrLots: number;
duration: number;
bidsDuration: number;
durationPerLots: string;
totalAmount: number;
averageAmount: string;
medianAmount: string;
minAmount: string;
maxAmount: string;
unsoldLots: number;
unsoldPercentage: string;
}
export interface Sale {

View File

@ -0,0 +1,10 @@
export interface User {
_id: string;
email: string;
isAdmin: boolean;
isAgent?: boolean;
username: string;
expiration?: string;
password?: string;
confirmPassword?: string;
}

View File

@ -55,7 +55,7 @@ export class LoginComponent implements OnInit {
this.router.navigate(['/']);
},
error => {
this.notificationService.openSnackBar(error.error);
this.notificationService.openSnackBar(error.error.message);
this.loading = false;
}
);

View File

@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { apiService } from 'src/app/core/services/api.service';
import { apiFavoriteService } from 'src/app/core/services/api/api.favorite.service';
@Component({
selector: 'app-favorites-page',
@ -16,7 +16,7 @@ export class FavoritesPageComponent implements OnInit {
constructor(
private router: Router,
private apiService: apiService) {}
private apiFavoriteService: apiFavoriteService) {}
openDialog(): void {
@ -25,7 +25,7 @@ export class FavoritesPageComponent implements OnInit {
}
ngOnInit(): void {
this.apiService.getAllFavorite().subscribe((data: any) => {
this.apiFavoriteService.getAllFavorite().subscribe((data: any) => {
this.dataSource = data;
});
}

View File

@ -7,7 +7,9 @@ import * as moment from 'moment-timezone';
//Services
import { NotificationService } from 'src/app/core/services/notification.service';
import { apiService } from 'src/app/core/services/api.service';
import { apiLotService } from 'src/app/core/services/api/api.lot.service';
import { apiFavoriteService } from 'src/app/core/services/api/api.favorite.service';
import { apiSaleService } from 'src/app/core/services/api/api.sale.service';
// Models
import { LotInfo } from 'src/app/core/services/model/lotInfo.interface';
@ -36,7 +38,9 @@ export class NewFavoritesPageComponent implements OnInit {
private ActivatedRoute: ActivatedRoute,
private router: Router,
private notificationService: NotificationService,
private apiService: apiService,) {
private apiLotService: apiLotService,
private apiSaleService: apiSaleService,
private apiFavoriteService: apiFavoriteService,) {
this.ActivatedRoute.params.subscribe(params => {
this.url = params['url'];
@ -80,11 +84,11 @@ export class NewFavoritesPageComponent implements OnInit {
// this.date = moment(this.SaleInfo.date).tz('Europe/Paris').toDate();
// this.hour = moment(this.SaleInfo.date).tz('Europe/Paris').format('HH:mm');
this.apiService.getLotInfo(this.url).subscribe( lotInfo => {
this.apiLotService.getLotInfo(this.url).subscribe( lotInfo => {
console.log(lotInfo);
this.lotInfo = lotInfo;
this.apiService.getSaleInfos(this.lotInfo.saleInfo.url).subscribe( SaleInfo => {
this.apiSaleService.getSaleInfos(this.lotInfo.saleInfo.url).subscribe( SaleInfo => {
console.log(SaleInfo);
this.SaleInfo = SaleInfo;
@ -99,7 +103,7 @@ export class NewFavoritesPageComponent implements OnInit {
}
);
this.apiService.getPictures(this.url).subscribe( pictures => {
this.apiLotService.getPictures(this.url).subscribe( pictures => {
this.images = pictures;
this.picture = pictures[0];
});
@ -127,7 +131,7 @@ export class NewFavoritesPageComponent implements OnInit {
// Europe/Paris is the timezone of the user
let dateTime = moment.tz(`${this.date.toISOString().split('T')[0]}T${this.hour}`, 'Europe/Paris').format();
this.apiService.saveFavorite(this.lotInfo, this.SaleInfo, this.picture, dateTime, this.buyProject, this.maxPrice, this.Note).subscribe( res => {
this.apiFavoriteService.saveFavorite(this.lotInfo, this.SaleInfo, this.picture, dateTime, this.buyProject, this.maxPrice, this.Note).subscribe( res => {
this.notificationService.openSnackBar("Favorite saved");
this.router.navigate(['favorites']);
});

View File

@ -1,6 +1,7 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { apiService } from 'src/app/core/services/api.service';
import { apiLotService } from 'src/app/core/services/api/api.lot.service';
@Component({
selector: 'app-pictures-page',
@ -20,7 +21,7 @@ export class PicturesPageComponent implements OnInit {
constructor(
private titleService: Title,
private apiService: apiService,
private apiLotService: apiLotService,
) {
}
@ -43,7 +44,7 @@ export class PicturesPageComponent implements OnInit {
}
getPictures(): void {
this.apiService.getPictures(this.url).subscribe( Pictures => {
this.apiLotService.getPictures(this.url).subscribe( Pictures => {
this.images = [];
const newImages = [...this.images];

View File

@ -3,7 +3,7 @@ import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import * as moment from 'moment-timezone';
//Services
import { apiService } from 'src/app/core/services/api.service';
import { apiSaleService } from 'src/app/core/services/api/api.sale.service';
// Models
import { SaleInfo } from 'src/app/core/services/model/saleInfo.interface';
@ -23,7 +23,7 @@ export class LoadingSaleDialogComponent implements OnInit {
hour: string = "";
constructor(
private apiService: apiService,
private apiSaleService: apiSaleService,
public dialogRef: MatDialogRef<LoadingSaleDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.SaleInfo = {
@ -44,7 +44,7 @@ export class LoadingSaleDialogComponent implements OnInit {
this.url = this.data.url;
console.log(this.url);
this.apiService.getSaleInfos(this.url).subscribe( SaleInfo => {
this.apiSaleService.getSaleInfos(this.url).subscribe( SaleInfo => {
console.log(SaleInfo);
this.SaleInfo = SaleInfo;
@ -70,7 +70,7 @@ export class LoadingSaleDialogComponent implements OnInit {
status: "ready"
}
this.apiService.saveSale(Sale).subscribe( Sale => {
this.apiSaleService.saveSale(Sale).subscribe( Sale => {
this.dialogRef.close(true);
});
}

View File

@ -0,0 +1,4 @@
.example-card {
margin-bottom: 8px;
}

View File

@ -0,0 +1,71 @@
<mat-card>
<mat-card-header>
<mat-card-title>Lot</mat-card-title>
<mat-card-subtitle>Lot informations</mat-card-subtitle>
</mat-card-header>
<mat-card-content>
<div fxLayout="row" fxLayoutGap="5px">
<mat-list>
<mat-list-item>
<span matListItemTitle>#</span>
<span matListItemLine>{{Lot.lotNumber}}</span>
</mat-list-item>
<mat-list-item>
<span matListItemTitle>Title</span>
<span matListItemLine>{{Lot.title}}</span>
</mat-list-item>
<mat-list-item>
<span matListItemTitle>Description</span>
<span matListItemLine [innerHTML]="getSafeDescription()"></span>
</mat-list-item>
<mat-list-item>
<span matListItemTitle>Estimate</span>
<span matListItemLine>Low: {{Lot.EstimateLow}} | High: {{Lot.EstimateHigh}} </span>
</mat-list-item>
</mat-list>
</div>
<div fxLayout="row" fxLayoutGap="5px">
<table mat-table [dataSource]="bids" class="mat-elevation-z8">
<!--- Note that these columns can be defined in any order.
The actual rendered columns are set as a property on the row definition" -->
<!-- Lotnum Column -->
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef >Amount</th>
<td mat-cell *matCellDef="let element">
{{element.amount}}
</td>
</ng-container>
<!-- Title Column -->
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef > Type </th>
<td mat-cell *matCellDef="let element">
{{element.auctioned_type}}
</td>
</ng-container>
<!-- Time Column -->
<ng-container matColumnDef="time">
<th mat-header-cell *matHeaderCellDef > Time </th>
<td mat-cell *matCellDef="let element">
{{element.timestamp | date:'HH:mm:ss':'Europe/Paris'}}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<mat-paginator [pageSizeOptions]="[5, 10, 15]" showFirstLastButtons></mat-paginator>
</mat-card-content>
</mat-card>
<mat-card>
<mat-card-content>
<div fxLayout="row" fxLayoutGap="5px">
<button mat-button color="warn" (click)="cancel()">Close</button>
</div>
</mat-card-content>
</mat-card>

View File

@ -0,0 +1,76 @@
import { Component, OnInit, Inject, ViewChild } from '@angular/core';
import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';import * as moment from 'moment-timezone';
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
//Services
import { apiLotService } from 'src/app/core/services/api/api.lot.service';
// Models
import { Lot, Bid } from 'src/app/core/services/model/lot.interface';
@Component({
selector: 'lot-detail-dialog-dialog',
templateUrl: './lot-detail-dialog.component.html',
styleUrls: ['./lot-detail-dialog.component.css']
})
export class LotDetailDialogComponent implements OnInit {
id: string;
Lot: Lot;
displayedColumns: string[] = ['amount', 'type', 'time'];
bids: MatTableDataSource<Bid> = new MatTableDataSource<Bid>();
@ViewChild(MatPaginator) paginator?: MatPaginator;
constructor(
private apiLotService: apiLotService,
public dialogRef: MatDialogRef<LotDetailDialogComponent>,
private sanitizer: DomSanitizer,
@Inject(MAT_DIALOG_DATA) public data: any) {
this.id = data.id;
this.Lot = {
_id: { $oid: '' },
idPlatform: '',
platform: '',
timestamp: '',
lotNumber: '',
RawData: {},
sale_id: { $oid: '' },
Bids: [],
title: '',
description: '',
auctioned: {
timestamp: '',
amount: 0,
auctioned_type: '',
sold: false
}
};
}
ngOnInit(): void {
this.apiLotService.getLot(this.id).subscribe( Lot => {
this.Lot = Lot;
this.bids = new MatTableDataSource(Lot.Bids ?? []);
this.bids.paginator = this.paginator ?? null;
});
}
getSafeDescription(): SafeHtml {
if (this.Lot && this.Lot.description) {
return this.sanitizer.bypassSecurityTrustHtml(this.Lot.description.replace(/\n/g, '<br>'));
}
return '';
}
cancel(): void {
this.dialogRef.close(false);
}
}

View File

@ -0,0 +1,9 @@
.mat-mdc-row .mat-mdc-cell {
border-bottom: 1px solid transparent;
border-top: 1px solid transparent;
cursor: pointer;
}
.mat-mdc-row:hover .mat-mdc-cell {
border-color: currentColor;
}

View File

@ -17,16 +17,23 @@
</div>
<div fxLayout="column" fxLayoutGap="2px">
<p><mat-icon>tag</mat-icon>{{Sale.postProcessing?.nbrLots}} Lots</p>
<p><mat-icon>hourglass_bottom</mat-icon>{{getDurationInMinutes(Sale.postProcessing?.duration ?? 0)}} min</p>
<p><mat-icon>hourglass_bottom</mat-icon>{{convertSecondsToHHMM(Sale.postProcessing?.bidsDuration ?? 0)}}</p>
<p><mat-icon>hourglass_bottom</mat-icon>/Lot {{getTimePerLot(parseToNumber(Sale.postProcessing?.durationPerLots ?? '0'))}}</p>
</div>
<div fxLayout="column" fxLayoutGap="2px">
<p>Total amount: {{Sale.postProcessing?.totalAmount | currency:'EUR':'symbol':'1.2-2'}}</p>
<p>Max amount: {{Sale.postProcessing?.maxAmount | currency:'EUR':'symbol':'1.2-2'}}</p>
<p>Min amount: {{Sale.postProcessing?.minAmount | currency:'EUR':'symbol':'1.2-2'}}</p>
</div>
<div fxLayout="column" fxLayoutGap="2px">
<p>Average amount: {{Sale.postProcessing?.averageAmount | currency:'EUR':'symbol':'1.2-2'}}</p>
<p>Median amount: {{Sale.postProcessing?.medianAmount | currency:'EUR':'symbol':'1.2-2'}}</p>
<p>Unsold: {{Sale.postProcessing?.unsoldLots}} ({{Sale.postProcessing?.unsoldPercentage}}%)</p>
</div>
</div>
</div>
<div fxLayout="row" fxLayoutGap="2px">
<button mat-raised-button color="primary" (click)="downloadExcelStatsFile(id)">Excel</button>
</div>
</mat-card-content>
</mat-card>
@ -48,12 +55,18 @@
<!-- Lotnum Column -->
<ng-container matColumnDef="lotNum">
<th mat-header-cell *matHeaderCellDef mat-sort-header="idPlatform">#</th>
<th mat-header-cell *matHeaderCellDef mat-sort-header="lotNumber">#</th>
<td mat-cell *matCellDef="let element">
{{element.lotNumber}}
</td>
</ng-container>
<!-- Picture Column -->
<ng-container matColumnDef="picture">
<th mat-header-cell *matHeaderCellDef> Picture </th>
<td mat-cell *matCellDef="let element"><img [src]="element.picture" alt="Picture" style="height: 60px"></td>
</ng-container>
<!-- Title Column -->
<ng-container matColumnDef="title">
<th mat-header-cell *matHeaderCellDef mat-sort-header="title"> Title </th>
@ -82,24 +95,48 @@
<ng-container matColumnDef="price">
<th mat-header-cell *matHeaderCellDef mat-sort-header="auctionedAmount"> Price </th>
<td mat-cell *matCellDef="let element">
{{element.auctionedAmount}}
<span [ngClass]="{'bold-text': element.auctionedAmount > 0}">{{element.auctionedAmount > 0 ? element.auctionedAmount : "-"}}</span>
</td>
</ng-container>
<!-- Price Column -->
<!-- nbrBids Column -->
<ng-container matColumnDef="nbrBids">
<th mat-header-cell *matHeaderCellDef mat-sort-header="bidsLength"> nbrBids </th>
<td mat-cell *matCellDef="let element">
{{element.bidsLength}}
{{element.postProcessing?.nbrBids}}
</td>
</ng-container>
<!-- duration Column -->
<ng-container matColumnDef="duration">
<th mat-header-cell *matHeaderCellDef mat-sort-header="duration"> Duration </th>
<td mat-cell *matCellDef="let element">
{{element.postProcessing?.duration}} s
</td>
</ng-container>
<!-- percentageAboveUnderLow Column -->
<ng-container matColumnDef="percentageAboveUnderLow">
<th mat-header-cell *matHeaderCellDef mat-sort-header="percentageAboveUnderLow">Above/Under Low</th>
<td mat-cell *matCellDef="let element">
{{element.postProcessing?.percentageAboveUnderLow}} %
</td>
</ng-container>
<!-- percentageAboveUnderHigh Column -->
<ng-container matColumnDef="percentageAboveUnderHigh">
<th mat-header-cell *matHeaderCellDef mat-sort-header="percentageAboveUnderHigh">Above/Under High</th>
<td mat-cell *matCellDef="let element">
{{element.postProcessing?.percentageAboveUnderHigh}} %
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" (click)="openDetailLot(row._id)"></tr>
</table>
</div>
<mat-paginator [pageSizeOptions]="[10, 50, 100]" showFirstLastButtons></mat-paginator>
<mat-paginator [pageSizeOptions]="[10, 50, 100]" showFirstLastButtons></mat-paginator>
</mat-tab>

View File

@ -4,17 +4,18 @@ import { Router, ActivatedRoute } from '@angular/router';
import { MatPaginator } from '@angular/material/paginator';
import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort';
import { LotDetailDialogComponent } from './lot-detail-dialog/lot-detail-dialog.component';
import * as moment from 'moment';
// Services
import { apiService } from 'src/app/core/services/api.service';
import { apiSaleService } from 'src/app/core/services/api/api.sale.service';
import { apiLotService } from 'src/app/core/services/api/api.lot.service';
import { NotificationService } from 'src/app/core/services/notification.service';
//Models
import { Sale } from 'src/app/core/services/model/sale.interface';
import { Lot } from 'src/app/core/services/model/lot.interface';
import { SaleInfo } from 'src/app/core/services/model/saleInfo.interface';
@Component({
selector: 'app-favorites-page',
@ -27,8 +28,8 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit {
id: any = '';
Sale: Sale;
displayedColumns: string[] = ['lotNum', 'title', 'estimateLow', 'estimateHigh', 'price', 'nbrBids'];
displayedColumns: string[] = ['lotNum', 'picture', 'title', 'estimateLow', 'estimateHigh', 'price', 'nbrBids', 'duration', 'percentageAboveUnderLow', 'percentageAboveUnderHigh'];
lotList: MatTableDataSource<Lot> = new MatTableDataSource<Lot>();
@ViewChild(MatPaginator) paginator?: MatPaginator;
@ViewChild(MatSort) sort?: MatSort;
@ -38,7 +39,8 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit {
private notificationService: NotificationService,
private router: Router,
public dialog: MatDialog,
private apiService: apiService) {
private apiSaleService: apiSaleService,
private apiLotService: apiLotService) {
this.Sale = {
_id: { $oid: '' },
@ -53,10 +55,15 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit {
postProcessing: {
nbrLots: 0,
duration: 0,
bidsDuration: 0,
durationPerLots: '',
totalAmount: 0,
averageAmount: '',
medianAmount: ''
medianAmount: '',
minAmount: '',
maxAmount: '',
unsoldLots: 0,
unsoldPercentage: ''
}
}
@ -76,17 +83,49 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit {
}
getSale(){
this.apiService.getSale(this.id).subscribe((sale: Sale) => {
this.apiSaleService.getSale(this.id).subscribe((sale: Sale) => {
this.Sale = sale;
});
}
getLotList(){
this.apiService.getLotsBySale(this.id).subscribe((lotList: Lot[]) => {
this.apiLotService.getLotsBySale(this.id).subscribe((lotList: Lot[]) => {
// adding the Bids length info
lotList = lotList.map((lot) => {
return {...lot, bidsLength: lot.Bids ? lot.Bids.length : 0};
return {...lot, bidsLength: lot.postProcessing ? lot.postProcessing.nbrBids : 0};
});
// adding the picture info
let path = '';
lotList = lotList.map((lot) => {
switch (lot.platform) {
case 'drouot':
path = 'https://cdn.drouot.com/d/image/lot?size=phare&path='+(lot.RawData as { photos?: { path: string }[] })?.photos?.[0]?.path;
return {...lot, picture: path ?? ''};
case 'interencheres':
path = (lot.RawData as { medias?: { lg: string }[] })?.medias?.[0]?.lg ?? '';
return {...lot, picture: path};
default:
return {...lot, picture: ''};
}
});
// adding the duration info
lotList = lotList.map((lot) => {
return {...lot, duration: lot.postProcessing ? lot.postProcessing.duration : 0};
});
// adding the percentageAboveUnderLow info
lotList = lotList.map((lot) => {
return {...lot, percentageAboveUnderLow: lot.postProcessing ? lot.postProcessing.percentageAboveUnderLow : 0};
});
// adding the percentageAboveUnderHigh info
lotList = lotList.map((lot) => {
return {...lot, percentageAboveUnderHigh: lot.postProcessing ? lot.postProcessing.percentageAboveUnderHigh : 0};
});
// adding the Auctionned ammount info
@ -101,9 +140,9 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit {
});
}
getDurationInMinutes(duration: number): number {
return Math.floor(duration / 60);
}
// getDurationInMinutes(duration: number): number {
// return Math.floor(duration / 60);
// }
getTimePerLot(duration: number): string {
@ -124,6 +163,30 @@ export class SaleDetailPageComponent implements OnInit, AfterViewInit {
return parseFloat(value || '0');
}
convertSecondsToHHMM(seconds: number): string {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}`;
}
openDetailLot(idLot: string): void {
this.dialog.open(LotDetailDialogComponent, {
width: '80%',
data: {id: idLot}
});
}
downloadExcelStatsFile(_id: String): void {
this.apiSaleService.getSaleStatXsl(_id).subscribe((data: Blob) => {
const downloadURL = window.URL.createObjectURL(data);
const link = document.createElement('a');
link.href = downloadURL;
link.download = 'SaleStats.xlsx';
link.click();
});
}
}

View File

@ -63,31 +63,6 @@
<td mat-cell *matCellDef="let element"> {{element.platform}} </td>
</ng-container>
<!-- Prepare Column -->
<!-- <ng-container matColumnDef="prepare">
<th mat-header-cell *matHeaderCellDef> Prepare </th>
<td mat-cell *matCellDef="let element">
<button *ngIf="element.status == 'ready'" mat-button (click)="prepareSale(element)"><mat-icon>format_list_numbered</mat-icon></button>
</td>
</ng-container> -->
<!-- Follow Column -->
<!-- <ng-container matColumnDef="follow">
<th mat-header-cell *matHeaderCellDef> Follow </th>
<td mat-cell *matCellDef="let element">
<button *ngIf="element.status == 'ready'" mat-button (click)="followSale(element)"><mat-icon>play_arrow</mat-icon></button>
<button *ngIf="element.status == 'following'" mat-button (click)="stopFollowSale(element)"><mat-icon>stop</mat-icon></button>
<div *ngIf="element.status == 'askStop'"><mat-progress-bar mode="indeterminate"></mat-progress-bar></div>
</ng-container> -->
<!-- Delete Column -->
<!-- <ng-container matColumnDef="delete">
<th mat-header-cell *matHeaderCellDef> Delete </th>
<td mat-cell *matCellDef="let element">
<button mat-button (click)="deleteSale(element._id)"><mat-icon>delete</mat-icon></button>
</td>
</ng-container> -->
<!-- Action Column -->
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef> Action </th>
@ -97,6 +72,8 @@
<mat-option *ngIf="element.status == 'ready'" (click)="followSale(element)"><mat-icon>play_arrow</mat-icon> Follow</mat-option>
<mat-option *ngIf="element.status == 'following'" (click)="stopFollowSale(element)"><mat-icon>stop</mat-icon> Stop Following</mat-option>
<mat-option *ngIf="element.status == 'following'" (click)="resetToReady(element._id)">Reset to Ready</mat-option>
<mat-option (click)="navigateToSaleDetail(element._id)"><mat-icon>info</mat-icon> Details</mat-option>
<mat-option (click)="postProcessing(element._id)"><mat-icon>query_stats</mat-icon> Post-processing</mat-option>
<mat-option (click)="deleteSale(element._id)"><mat-icon>delete</mat-icon> Delete</mat-option>
</mat-select>
</td>

View File

@ -1,12 +1,13 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { Title } from '@angular/platform-browser';
import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { LoadingSaleDialogComponent } from './loading-sale-dialog/loading-sale-dialog.component';
import * as moment from 'moment';
// Services
import { apiService } from 'src/app/core/services/api.service';
import { NotificationService } from 'src/app/core/services/notification.service';
import { apiSaleService } from 'src/app/core/services/api/api.sale.service';
import { Sale } from 'src/app/core/services/model/sale.interface';
@ -15,7 +16,7 @@ import { Sale } from 'src/app/core/services/model/sale.interface';
templateUrl: './sales-page.component.html',
styleUrls: ['./sales-page.component.css']
})
export class SalesPageComponent implements OnInit {
export class SalesPageComponent implements OnInit,OnDestroy {
url: string = '';
refreshSalesId: any;
@ -29,7 +30,8 @@ export class SalesPageComponent implements OnInit {
private notificationService: NotificationService,
private router: Router,
public dialog: MatDialog,
private apiService: apiService) {}
private apiSaleService: apiSaleService,
private titleService: Title) {}
openLoadingSale(): void {
const dialogRef = this.dialog.open(LoadingSaleDialogComponent, {
@ -47,7 +49,7 @@ export class SalesPageComponent implements OnInit {
}
refreshSales(): void {
this.apiService.getAllSale().subscribe((data: any) => {
this.apiSaleService.getAllSale().subscribe((data: any) => {
console.log(data);
const today = moment().startOf('day');
@ -62,15 +64,21 @@ export class SalesPageComponent implements OnInit {
}
ngOnInit(): void {
this.titleService.setTitle('Jucundus - Sales');
this.refreshSales();
this.refreshSalesId = setInterval(() => {
this.refreshSales();
}, 5000);
}
ngOnDestroy() {
if (this.refreshSalesId) {
clearInterval(this.refreshSalesId);
}
}
prepareSale(Sale: Sale): void {
this.apiService.prepareSale(Sale).subscribe( data => {
this.apiSaleService.prepareSale(Sale).subscribe( data => {
console.log(data);
this.refreshSales();
this.notificationService.openSnackBar("Prepare Sale");
@ -80,7 +88,7 @@ export class SalesPageComponent implements OnInit {
followSale(Sale: Sale): void {
this.apiService.followSale(Sale).subscribe( data => {
this.apiSaleService.followSale(Sale).subscribe( data => {
console.log(data);
this.refreshSales();
this.notificationService.openSnackBar("Sale followed");
@ -90,21 +98,21 @@ export class SalesPageComponent implements OnInit {
stopFollowSale(Sale: Sale): void {
Sale.status = "askStop";
this.apiService.updateSale(Sale).subscribe( data => {
this.apiSaleService.updateSale(Sale).subscribe( data => {
this.refreshSales();
this.notificationService.openSnackBar("Sale Stopping...");
})
}
deleteSale(_id: string): void {
this.apiService.deleteSale(_id).subscribe( data => {
this.apiSaleService.deleteSale(_id).subscribe( data => {
this.refreshSales();
this.notificationService.openSnackBar("Sale deleted");
})
}
resetToReady(_id: string): void {
this.apiService.resetSaleToReady(_id)
this.apiSaleService.resetSaleToReady(_id)
}
navigateToSaleDetail(id: string) {
@ -114,7 +122,7 @@ export class SalesPageComponent implements OnInit {
}
postProcessing(id: string) {
this.apiService.postProcessing(id).subscribe( data => {
this.apiSaleService.postProcessing(id).subscribe( data => {
this.notificationService.openSnackBar("Sale processing");
})
}

View File

@ -5,13 +5,15 @@ import { SalesRoutingModule } from './sales-routing.module';
import { SalesPageComponent } from './sales-page/sales-page.component';
import { LoadingSaleDialogComponent } from './sales-page/loading-sale-dialog/loading-sale-dialog.component';
import { SaleDetailPageComponent } from './sales-page/sale-detail-page/sale-detail-page.component';
import { LotDetailDialogComponent } from './sales-page/sale-detail-page/lot-detail-dialog/lot-detail-dialog.component';
import { SharedModule } from '../../shared/shared.module';
@NgModule({
declarations: [
SalesPageComponent,
LoadingSaleDialogComponent,
SaleDetailPageComponent
SaleDetailPageComponent,
LotDetailDialogComponent
],
imports: [
CommonModule,

View File

@ -0,0 +1,9 @@
.mat-mdc-row .mat-mdc-cell {
border-bottom: 1px solid transparent;
border-top: 1px solid transparent;
cursor: pointer;
}
.mat-mdc-row:hover .mat-mdc-cell {
border-color: currentColor;
}

View File

@ -0,0 +1,60 @@
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="95%">
<mat-toolbar>
<h2><a routerLink="/users">Users</a>/{{id !== '' ? 'Edit' : 'New'}}</h2>
</mat-toolbar>
<mat-card>
<mat-card-header>
<mat-card-title>Sale Detail</mat-card-title>
</mat-card-header>
<mat-card-content>
<form [formGroup]="userForm">
<div fxLayout="row" fxLayoutGap="5px">
<mat-form-field fxFlex="50">
<input matInput placeholder="Username" formControlName="username" required>
<mat-error *ngIf="userForm.get('username')?.hasError('required')">
Username is <strong>required</strong>
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="50">
<input matInput placeholder="Email" formControlName="email" required>
<mat-error *ngIf="userForm.get('email')?.hasError('email') && !userForm.get('email')?.hasError('required')">
Please enter a valid email address
</mat-error>
<mat-error *ngIf="userForm.get('email')?.hasError('required')">
Email is <strong>required</strong>
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="5px">
<mat-form-field fxFlex="50">
<input matInput placeholder="Password" formControlName="password">
<mat-error *ngIf="userForm.get('password')?.hasError('passwordLength')">
Password must be at least 8 characters long
</mat-error>
<mat-error *ngIf="userForm.get('password')?.hasError('mismatch') || userForm.get('confirmPassword')?.value === null">
Passwords do not match
</mat-error>
</mat-form-field>
<mat-form-field fxFlex="50">
<input matInput placeholder="Confirm Password" formControlName="confirmPassword">
<mat-error *ngIf="userForm.errors?.['mismatch']">
Passwords do not match
</mat-error>
</mat-form-field>
</div>
<div fxLayout="row" fxLayoutGap="5px">
<mat-checkbox name="isAdmin" formControlName="isAdmin">Admin</mat-checkbox>
<mat-checkbox name="isAgent" formControlName="isAgent">Agent</mat-checkbox>
</div>
<div fxLayout="row" fxLayoutGap="5px">
<button mat-raised-button color="primary" type="submit" [disabled]="!userForm.valid" (click)="save()">Save</button>
<button mat-raised-button color="warn" type="button" *ngIf="id" (click)="deleteUser()">Delete</button>
</div>
</form>
</mat-card-content>
</mat-card>
</div>
</div>

View File

@ -0,0 +1,123 @@
import { Component, OnInit } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { Router, ActivatedRoute } from '@angular/router';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ConfirmDialogComponent, ConfirmDialogModel } from 'src/app/shared/confirm-dialog/confirm-dialog.component';
// Services
import { NotificationService } from 'src/app/core/services/notification.service';
import { apiUserService } from 'src/app/core/services/api/api.user.service';
function passwordMatchValidator(g: FormGroup) {
const password = g.get('password')?.value;
const confirmPassword = g.get('confirmPassword')?.value;
if (password){
if (password.length < 8) {
g.get('password')?.setErrors({ passwordLength: true });
return { passwordLength: true };
}
if (password !== confirmPassword) {
g.get('confirmPassword')?.setErrors({ mismatch: true });
return { mismatch: true };
}
}
return null;
}
@Component({
selector: 'app-favorites-page',
templateUrl: './user-edit-page.component.html',
styleUrls: ['./user-edit-page.component.css']
})
export class UserEditPageComponent implements OnInit {
id: any = '';
userForm: FormGroup;
constructor(
private route: ActivatedRoute,
private notificationService: NotificationService,
private router: Router,
public dialog: MatDialog,
private apiUserService: apiUserService,
private fb: FormBuilder
) {
this.userForm = this.fb.group({
username: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
password: [''],
confirmPassword: [''],
isAdmin: [false],
isAgent: [false]
}, { validator: passwordMatchValidator });
}
ngOnInit(): void {
this.route.paramMap.subscribe(params => {
this.id = params.get('id');
if(this.id) {
this.apiUserService.getUser(this.id).subscribe((data: any) => {
console.log(data);
// Update form values
this.userForm.patchValue({
username: data.username,
email: data.email,
isAdmin: data.isAdmin,
isAgent: data.isAgent
});
});
}
});
}
save(): void {
if (this.userForm.valid) {
let userData = this.userForm.value
console.log(userData);
if(this.id) {
userData._id = this.id;
// Edit User
this.apiUserService.updateUser(userData).subscribe((data: any) => {
this.notificationService.openSnackBar("User updated successfully");
this.router.navigate(['/users']);
});
} else {
// New User
this.apiUserService.saveUser(userData).subscribe((data: any) => {
this.notificationService.openSnackBar("User created successfully");
this.router.navigate(['/users']);
});
}
}
}
deleteUser(): void {
if (this.id) {
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
width: '250px',
data: {title: 'Delete User ?', message: 'Are you sure you want to delete this user?'}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
// User clicked Yes
this.apiUserService.deleteUser(this.id).subscribe((data: any) => {
this.notificationService.openSnackBar("User deleted successfully");
this.router.navigate(['/users']);
});
}
});
}
}
}

View File

@ -1,18 +1,61 @@
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="95%">
<mat-toolbar>
<mat-toolbar-row>
<h2>Users</h2>
</mat-toolbar-row>
<mat-toolbar-row>
<button mat-raised-button color="primary" (click)="openUser('')">Add User</button>
</mat-toolbar-row>
</mat-toolbar>
<mat-card>
<mat-card-content>
<h2>Users</h2>
<div fxLayout="row" fxLayoutGap="5px">
<table mat-table [dataSource]="users" matSort class="mat-elevation-z8">
<div class="container" fxLayout="row" fxLayoutAlign="center none">
<div fxFlex="50%" class="text-center no-records animate">
<mat-icon>people_outline</mat-icon>
<p>No users exist.</p>
</div>
<mat-icon> </mat-icon>
</div>
<!--- Note that these columns can be defined in any order.
The actual rendered columns are set as a property on the row definition" -->
<!-- username Column -->
<ng-container matColumnDef="username">
<th mat-header-cell *matHeaderCellDef mat-sort-header="username">Username</th>
<td mat-cell *matCellDef="let element">
{{element.username}}
</td>
</ng-container>
<!-- email Column -->
<ng-container matColumnDef="email">
<th mat-header-cell *matHeaderCellDef mat-sort-header="email"> Email </th>
<td mat-cell *matCellDef="let element">
{{element.email}}
</td>
</ng-container>
<!-- Admin Column -->
<ng-container matColumnDef="admin">
<th mat-header-cell *matHeaderCellDef mat-sort-header="isAdmin"> Admin </th>
<td mat-cell *matCellDef="let element">
<mat-icon *ngIf="element.isAdmin" style="color:green;">check</mat-icon>
</td>
</ng-container>
<!-- Agent Column -->
<ng-container matColumnDef="agent">
<th mat-header-cell *matHeaderCellDef mat-sort-header="isAgent"> Agent </th>
<td mat-cell *matCellDef="let element">
<mat-icon *ngIf="element.isAgent" style="color:green;">check</mat-icon>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;" (click)="openUser(row._id)"></tr>
</table>
</div>
<mat-paginator [pageSizeOptions]="[10, 50, 100]" showFirstLastButtons></mat-paginator>
</mat-card-content>
</mat-card>

View File

@ -1,8 +1,14 @@
import { Component, OnInit } from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { Title } from '@angular/platform-browser';
import { NGXLogger } from 'ngx-logger';
import { NotificationService } from 'src/app/core/services/notification.service';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { apiUserService } from 'src/app/core/services/api/api.user.service';
import { User } from 'src/app/core/services/model/user.interface';
@Component({
selector: 'app-user-list',
@ -11,14 +17,38 @@ import { NotificationService } from 'src/app/core/services/notification.service'
})
export class UserListComponent implements OnInit {
displayedColumns: string[] = ['username', 'email', 'admin', 'agent'];
users : MatTableDataSource<User> = new MatTableDataSource<User>();
@ViewChild(MatPaginator) paginator?: MatPaginator;
@ViewChild(MatSort) sort?: MatSort;
constructor(
private logger: NGXLogger,
private notificationService: NotificationService,
private titleService: Title
private titleService: Title,
private router: Router,
private apiUserService: apiUserService,
) { }
ngOnInit() {
this.titleService.setTitle('Jucundus - Users');
this.logger.log('Users loaded');
this.refreshUsers()
}
refreshUsers(): void {
this.apiUserService.getAllUsers().subscribe((data: any) => {
console.log(data);
this.users = new MatTableDataSource(data);
this.users.paginator = this.paginator ?? null;
this.users.sort = this.sort ?? null;
this.notificationService.openSnackBar("Users loaded");
});
}
openUser(id: string): void {
this.router.navigate(['/users/edit', id]);
}
}

View File

@ -3,6 +3,7 @@ import { Routes, RouterModule } from '@angular/router';
import { LayoutComponent } from 'src/app/shared/layout/layout.component';
import { UserListComponent } from './user-list/user-list.component';
import { UserEditPageComponent } from './user-list/user-edit-page/user-edit-page.component';
const routes: Routes = [
{
@ -10,6 +11,7 @@ const routes: Routes = [
component: LayoutComponent,
children: [
{ path: '', component: UserListComponent },
{ path: 'edit/:id', component: UserEditPageComponent },
]
}
];

View File

@ -3,6 +3,8 @@ import { CommonModule } from '@angular/common';
import { UsersRoutingModule } from './users-routing.module';
import { UserListComponent } from './user-list/user-list.component';
import { UserEditPageComponent } from './user-list/user-edit-page/user-edit-page.component';
import { SharedModule } from 'src/app/shared/shared.module';
@NgModule({
@ -11,6 +13,9 @@ import { SharedModule } from 'src/app/shared/shared.module';
SharedModule,
UsersRoutingModule
],
declarations: [UserListComponent]
declarations: [
UserListComponent,
UserEditPageComponent
]
})
export class UsersModule { }

View File

@ -83,7 +83,12 @@
</mat-icon>
<p mat-line> Pictures </p>
</a>
<a mat-list-item [routerLink]="['/users']" routerLinkActive="active">
<mat-icon matListItemIcon>
people
</mat-icon>
<p mat-line> Users </p>
</a>
<!-- <a mat-list-item [routerLink]="['/customers']" routerLinkActive="active">
<mat-icon matListItemIcon>
@ -91,16 +96,10 @@
</mat-icon>
<p mat-line> Customers </p>
</a>
<a mat-list-item [routerLink]="['/users']" routerLinkActive="active">
<mat-icon matListItemIcon>
people
</mat-icon>
<p mat-line> Users </p>
</a> -->
-->
<mat-divider></mat-divider>
<h3 mat-subheader>User</h3>
<a mat-list-item [routerLink]="['/account/profile']">
<mat-icon matListItemIcon>person</mat-icon>

View File

@ -3,5 +3,6 @@ import { NgxLoggerLevel } from 'ngx-logger';
export const environment = {
production: true,
logLevel: NgxLoggerLevel.OFF,
serverLogLevel: NgxLoggerLevel.ERROR
serverLogLevel: NgxLoggerLevel.ERROR,
ServeurURL: "http://localhost:3000"
};

View File

@ -8,5 +8,6 @@ import { NgxLoggerLevel } from 'ngx-logger';
export const environment = {
production: false,
logLevel: NgxLoggerLevel.TRACE,
serverLogLevel: NgxLoggerLevel.OFF
serverLogLevel: NgxLoggerLevel.OFF,
ServeurURL: "http://localhost:3000"
};

View File

@ -16,6 +16,7 @@ services:
- ./backend:/backend
ports:
- "3000:3000"
- "9229:9229"
command: ["npm", "run", "dev"]
depends_on:
- db