first commit
This commit is contained in:
commit
adf8283ae0
|
|
@ -0,0 +1,10 @@
|
|||
backend/node_modules
|
||||
|
||||
client/.angular
|
||||
client/node_modules
|
||||
client/.vscode
|
||||
|
||||
.vscode
|
||||
data/
|
||||
vendore/
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
## Backend
|
||||
# Dev
|
||||
npm run dev
|
||||
http://localhost:3000
|
||||
|
||||
## Docker Database Dev
|
||||
```bash
|
||||
docker-compose -f docker-compose-dev.yml build
|
||||
docker-compose -f docker-compose-dev.yml up
|
||||
```
|
||||
|
||||
## Agenda
|
||||
http://localhost:3000/dash/
|
||||
|
||||
## API
|
||||
|
||||
# Lot
|
||||
GET http://localhost:3000/api/lot/getInfos/https%3A%2F%2Fwww.interencheres.com%2Fvehicules%2Fvehicules-624955%2Flot-75622389.html
|
||||
GET http://localhost:3000/api/lot/getPictures/https%3A%2F%2Fwww.interencheres.com%2Fvehicules%2Fvehicules-624955%2Flot-75622389.html
|
||||
|
||||
POST http://localhost:3000/api/lot/NextItem
|
||||
POST http://localhost:3000/api/lot/AuctionedItem
|
||||
POST http://localhost:3000/api/lot/Bid
|
||||
|
||||
# Sale
|
||||
GET http://localhost:3000/api/sale/getSaleInfos/https%3A%2F%2Fwww.interencheres.com%2Fvehicules%2Fvehicules-624955
|
||||
GET http://localhost:3000/api/sale/followSale/624955
|
||||
|
||||
GET http://localhost:3000/api/sale/sale/624955
|
||||
POST http://localhost:3000/api/sale/sale
|
||||
PUT http://localhost:3000/api/sale/sale/624955
|
||||
DELETE http://localhost:3000/api/sale/sale/624955
|
||||
|
||||
GET http://localhost:3000/api/sale/getAll
|
||||
GET http://localhost:3000/api/sale/getByUrl/https%3A%2F%2Fwww.interencheres.com%2Fvehicules%2Fvehicules-624955
|
||||
|
||||
# Favorite
|
||||
POST http://localhost:3000/api/favorite/save
|
||||
GET http://localhost:3000/api/favorite/getAll
|
||||
|
||||
# Prod
|
||||
npm run start
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const asyncHandler = require("express-async-handler");
|
||||
const { save, getAll } = require("../services/favorites");
|
||||
|
||||
exports.save = asyncHandler(async (req, res, next) => {
|
||||
|
||||
try{
|
||||
let result = await save(req.body);
|
||||
console.log(result);
|
||||
res.status(204).send();
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
return res.status(500).send(err);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
exports.getAll = asyncHandler(async (req, res, next) => {
|
||||
console.log("controller getAll");
|
||||
try{
|
||||
let result = await getAll();
|
||||
console.log(result);
|
||||
res.status(200).send(result);
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
return res.status(500).send(err);
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
const asyncHandler = require("express-async-handler");
|
||||
const fetch = require('node-fetch');
|
||||
const { LotDb } = require("../services/lotDb");
|
||||
const lotDb = new LotDb();
|
||||
const { SaleDb } = require("../services/saleDb");
|
||||
const saleDb = new SaleDb();
|
||||
|
||||
const ApiURL = "http://host.docker.internal:3020/api";
|
||||
|
||||
// scrapping
|
||||
exports.getInfos = asyncHandler(async (req, res, next) => {
|
||||
let url = req.params.url
|
||||
|
||||
url = encodeURIComponent(url);
|
||||
|
||||
fetch(ApiURL+'/lot/getInfos/'+url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
res.json(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
exports.getPictures = asyncHandler(async (req, res, next) => {
|
||||
let url = req.params.url
|
||||
|
||||
url = encodeURIComponent(url);
|
||||
|
||||
fetch(ApiURL+'/lot/getPictures/'+url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
res.json(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
exports.getLotsBySale = asyncHandler(async (req, res, next) => {
|
||||
let id = req.params.id
|
||||
|
||||
const 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);
|
||||
res.json(Lots);
|
||||
|
||||
});
|
||||
|
||||
// Follow Sale
|
||||
exports.NextItem = asyncHandler(async (req, res, next) => {
|
||||
try{
|
||||
|
||||
|
||||
Sale = await saleDb.getByIDPlatform(req.body.idSalePlatform, req.body.platform);
|
||||
if(!Sale){
|
||||
console.error("Sale not found");
|
||||
return res.status(404).send("Sale not found");
|
||||
}
|
||||
|
||||
let Lot = await lotDb.getByIDPlatform(req.body.idPlatform, req.body.platform);
|
||||
if(Lot == null){
|
||||
console.log("Creating new Lot");
|
||||
|
||||
Lot = {
|
||||
idPlatform: String(req.body.idPlatform),
|
||||
platform: req.body.platform,
|
||||
timestamp: req.body.timestamp,
|
||||
lotNumber: String(req.body.lotNumber),
|
||||
title: req.body.title,
|
||||
description: req.body.description,
|
||||
EstimateLow: req.body.EstimateLow,
|
||||
EstimateHigh: req.body.EstimateHigh,
|
||||
RawData: req.body.RawData,
|
||||
sale_id: Sale._id,
|
||||
}
|
||||
|
||||
await lotDb.post(Lot);
|
||||
|
||||
}else{
|
||||
console.log("Updating Lot");
|
||||
|
||||
Lot.timestamp = req.body.timestamp;
|
||||
Lot.lotNumber = String(req.body.lotNumber);
|
||||
Lot.title = req.body.title;
|
||||
Lot.description = req.body.description;
|
||||
Lot.EstimateLow = req.body.EstimateLow;
|
||||
Lot.EstimateHigh = req.body.EstimateHigh;
|
||||
Lot.RawData = req.body.RawData;
|
||||
|
||||
await lotDb.put(Lot._id, Lot);
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
return res.status(500).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
exports.Bid = asyncHandler(async (req, res, next) => {
|
||||
try{
|
||||
let Lot = await lotDb.getByIDPlatform(req.body.idPlatform, req.body.platform);
|
||||
|
||||
if(Lot){
|
||||
console.log("Update Lot Bid");
|
||||
|
||||
BidInfo = {
|
||||
timestamp: req.body.timestamp,
|
||||
amount: req.body.amount,
|
||||
auctioned_type: req.body.auctioned_type,
|
||||
}
|
||||
// If Lot.BidInfo doesn't exist, initialize it as an empty array
|
||||
if (!Lot.Bids) {
|
||||
Lot.Bids = [];
|
||||
}
|
||||
|
||||
// Add BidInfo to the array
|
||||
Lot.Bids.push(BidInfo);
|
||||
|
||||
await lotDb.put(Lot._id, Lot);
|
||||
}else{
|
||||
console.error("Lot not found");
|
||||
return res.status(404).send("Lot not found");
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
return res.status(500).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
exports.AuctionedItem = asyncHandler(async (req, res, next) => {
|
||||
try{
|
||||
|
||||
let Lot = await lotDb.getByIDPlatform(req.body.idPlatform, req.body.platform);
|
||||
|
||||
if(Lot){
|
||||
console.log("Update Lot AuctionedItem");
|
||||
|
||||
Lot.auctioned = {
|
||||
timestamp: req.body.timestamp,
|
||||
amount: req.body.amount,
|
||||
auctioned_type: req.body.auctioned_type,
|
||||
sold: req.body.sold,
|
||||
}
|
||||
|
||||
await lotDb.put(Lot._id, Lot);
|
||||
}else{
|
||||
console.error("Lot not found");
|
||||
return res.status(404).send("Lot not found");
|
||||
}
|
||||
|
||||
res.status(204).send();
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
return res.status(500).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
const asyncHandler = require("express-async-handler");
|
||||
const moment = require('moment-timezone');
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { SaleDb } = require("../services/saleDb");
|
||||
const saleDb = new SaleDb();
|
||||
const { LotDb } = require("../services/lotDb");
|
||||
const lotDb = new LotDb();
|
||||
const agenda = require('../services/agenda');
|
||||
const {Agent} = require('../services/agent');
|
||||
const agent = new Agent();
|
||||
|
||||
|
||||
exports.getSaleInfos = asyncHandler(async (req, res, next) => {
|
||||
let url = req.params.url
|
||||
agent.getSaleInfos(url)
|
||||
.then(data => {
|
||||
return res.status(200).json(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
return res.status(500).send(error);
|
||||
});
|
||||
// url = encodeURIComponent(url);
|
||||
|
||||
// fetch(ApiAgentURL+'/sale/getSaleInfos/'+url)
|
||||
// .then(response => response.json())
|
||||
// .then(data => {
|
||||
// res.json(data);
|
||||
// })
|
||||
// .catch(error => {
|
||||
// console.error(error);
|
||||
// });
|
||||
});
|
||||
|
||||
exports.prepareSale = asyncHandler(async (req, res, next) => {
|
||||
|
||||
try{
|
||||
const id = req.params.id;
|
||||
|
||||
agent.prepareSale(id)
|
||||
.then(data => {
|
||||
return res.status(200).json({"message": "Lots created"});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
return res.status(500).send(error);
|
||||
});
|
||||
// url = encodeURIComponent(url);
|
||||
// fetch(ApiAgentURL+'/sale/getLotList/'+url)
|
||||
// .then(response => response.json())
|
||||
// .then(async data => {
|
||||
// console.log(data);
|
||||
// for (let lot of data) {
|
||||
// lot.sale_id = Sale._id
|
||||
|
||||
// await lotDb.post(lot);
|
||||
// }
|
||||
// res.status(200).send({"message": "Lots created"})
|
||||
// })
|
||||
// .catch(error => {
|
||||
// console.error(error);
|
||||
// return res.status(500).send(error);
|
||||
// });
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
return res.status(500).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
exports.followSale = asyncHandler(async (req, res, next) => {
|
||||
|
||||
try{
|
||||
const id = req.params.id;
|
||||
|
||||
agent.followSale(id)
|
||||
.then(data => {
|
||||
res.status(200).send(data);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
return res.status(500).send(error);
|
||||
});
|
||||
}catch(err){
|
||||
console.error(err);
|
||||
return res.status(500).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// DB
|
||||
exports.get = asyncHandler(async (req, res, next) => {
|
||||
|
||||
try{
|
||||
const id = req.params.id;
|
||||
let result = await saleDb.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 Sale = await saleDb.getByIDPlatform(req.body.idPlatform, req.body.platform);
|
||||
if(Sale){
|
||||
return res.status(500).send("Sale already exists");
|
||||
}
|
||||
|
||||
let createData = await saleDb.post(req.body);
|
||||
console.log(createData.insertedId);
|
||||
|
||||
const NowParis = moment.tz(new Date(),"Europe/Paris")
|
||||
|
||||
// Scheduling the Prepare job
|
||||
const dateSaleMinus24Hours = moment.tz(req.body.date, "Europe/Paris").subtract(24, 'hours');
|
||||
if(dateSaleMinus24Hours.isAfter(NowParis)){
|
||||
const jobPrepare = agenda.create('prepareSale', { saleId: createData.insertedId });
|
||||
jobPrepare.schedule(dateSaleMinus24Hours.toDate());
|
||||
await jobPrepare.save();
|
||||
}else{ console.log("Sale is less than 24 hours away, no Prepare Job");}
|
||||
|
||||
// Scheduling the Follow job
|
||||
const dateSale = moment.tz(req.body.date, "Europe/Paris");
|
||||
if(dateSale.isAfter(NowParis)){
|
||||
const jobFollow = agenda.create('followSale', { saleId: createData.insertedId });
|
||||
jobFollow.schedule(dateSale.toDate());
|
||||
await jobFollow.save();
|
||||
}else{ console.log("Sale is in the past, no Follow Job");}
|
||||
|
||||
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 saleDb.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 all lots linked to the sale
|
||||
console.log("Deleting lots sale_id: "+id);
|
||||
await lotDb.deleteAllLotBySaleId(id);
|
||||
|
||||
// Remove the sale
|
||||
await saleDb.remove(id);
|
||||
|
||||
//remove the Jobs
|
||||
const JobSale = await agenda.jobs({ 'data.saleId': new ObjectId(id) });
|
||||
for (const job of JobSale) {
|
||||
await job.remove();
|
||||
}
|
||||
|
||||
res.status(200).send({"message": "Sale and Lots deleted"});
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
return res.status(500).send(err);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Fucntions
|
||||
|
||||
exports.getAll = asyncHandler(async (req, res, next) => {
|
||||
try{
|
||||
let result = await saleDb.getAll();
|
||||
res.status(200).send(result);
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
return res.status(500).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
exports.getByUrl = asyncHandler(async (req, res, next) => {
|
||||
try{
|
||||
let url = req.params.url
|
||||
url = decodeURIComponent(url);
|
||||
|
||||
let result = await saleDb.getByUrl(url);
|
||||
//console.log(result);
|
||||
res.status(200).send(result);
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
return res.status(500).send(err);
|
||||
}
|
||||
});
|
||||
|
||||
exports.postProcessing = 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);
|
||||
|
||||
let startTime = 0;
|
||||
if (Array.isArray(Lots[0].Bids)) {
|
||||
startTime = Lots[0].Bids[0].timestamp;
|
||||
}else{
|
||||
startTime = 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;
|
||||
}else{
|
||||
endTime = LastBid.timestamp;
|
||||
}
|
||||
console.log("Start Time: "+startTime);
|
||||
console.log("End Time: "+endTime);
|
||||
|
||||
let duration = endTime-startTime;
|
||||
|
||||
let totalAmount = 0;
|
||||
for (let lot of Lots) {
|
||||
if (lot.auctioned) {
|
||||
totalAmount += lot.auctioned.amount;
|
||||
}
|
||||
}
|
||||
|
||||
function calculateMedian(array) {
|
||||
array.sort((a, b) => a - b);
|
||||
let middleIndex = Math.floor(array.length / 2);
|
||||
|
||||
if (array.length % 2 === 0) { // array has an even length
|
||||
return (array[middleIndex - 1] + array[middleIndex]) / 2;
|
||||
} else { // array has an odd length
|
||||
return array[middleIndex];
|
||||
}
|
||||
}
|
||||
const amounts = Lots.map(lot => lot.auctioned?.amount).filter(Boolean);
|
||||
|
||||
//console.error(Lots);
|
||||
let postProcessing = {
|
||||
nbrLots: Lots.length,
|
||||
duration: duration,
|
||||
durationPerLots: (duration/Lots.length).toFixed(0),
|
||||
totalAmount: totalAmount,
|
||||
averageAmount: (totalAmount/Lots.length).toFixed(2),
|
||||
medianAmount: calculateMedian(amounts).toFixed(2),
|
||||
}
|
||||
|
||||
console.log(postProcessing);
|
||||
|
||||
Sale.postProcessing = postProcessing;
|
||||
await saleDb.put(Sale._id, Sale);
|
||||
|
||||
res.status(200).send({"message": "Post Processing done"});
|
||||
}catch(err){
|
||||
console.log(err);
|
||||
return res.status(500).send(err);
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
const express = require('express')
|
||||
const app = express()
|
||||
|
||||
var bodyParser = require('body-parser');
|
||||
app.use(bodyParser.json())
|
||||
|
||||
const cors = require('cors');
|
||||
app.use(cors());
|
||||
// Enable preflight requests for all routes
|
||||
app.options('*', cors());
|
||||
|
||||
// Agenda Scheduller
|
||||
const agenda = require('./services/agenda');
|
||||
(async function() {
|
||||
await agenda.start();
|
||||
})();
|
||||
|
||||
// Agenda UI
|
||||
var Agendash = require("agendash");
|
||||
app.use("/dash", Agendash(agenda));
|
||||
|
||||
// routes
|
||||
app.use('/api/lot', require('./routes/lot'));
|
||||
app.use('/api/sale', require('./routes/sale'));
|
||||
app.use('/api/favorite', require('./routes/favorite'));
|
||||
|
||||
module.exports = app
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"name": "Attach to Process",
|
||||
"restart": true,
|
||||
"address": "127.0.0.1",
|
||||
"port": 53481,
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "${workspaceFolder}"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"name": "jucundus",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node server.js",
|
||||
"dev": "nodemon --watch ./ server.js --ignore node_modules/"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@angular/cli": "^17.1.3",
|
||||
"@hokify/agenda": "^6.3.0",
|
||||
"agendash": "^4.0.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"express-async-handler": "^1.2.0",
|
||||
"mongodb": "^6.5.0",
|
||||
"node-fetch": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.3"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
const controllers = require('../controllers/favorite')
|
||||
const router = require('express').Router()
|
||||
|
||||
router.post('/save/', controllers.save)
|
||||
router.get('/getAll/', controllers.getAll)
|
||||
|
||||
module.exports = router
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
const controllers = require('../controllers/lot')
|
||||
const router = require('express').Router()
|
||||
|
||||
router.get('/getInfos/:url', controllers.getInfos)
|
||||
router.get('/getPictures/:url', controllers.getPictures)
|
||||
router.get('/getLotsBySale/:id', controllers.getLotsBySale)
|
||||
|
||||
router.post('/NextItem/', controllers.NextItem)
|
||||
router.post('/AuctionedItem/', controllers.AuctionedItem)
|
||||
router.post('/Bid/', controllers.Bid)
|
||||
|
||||
module.exports = router
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
const controllers = require('../controllers/sale')
|
||||
const router = require('express').Router()
|
||||
|
||||
// AuctionAgent
|
||||
router.get('/getSaleInfos/:url', controllers.getSaleInfos)
|
||||
router.get('/prepareSale/:id', controllers.prepareSale)
|
||||
router.get('/followSale/:id', controllers.followSale)
|
||||
|
||||
|
||||
// DB
|
||||
router.get('/sale/:id', controllers.get)
|
||||
router.post('/sale/', controllers.post)
|
||||
router.put('/sale/:id', controllers.put)
|
||||
router.delete('/sale/:id', controllers.delete)
|
||||
|
||||
router.get('/getAll/', controllers.getAll)
|
||||
router.get('/getByUrl/:url', controllers.getByUrl)
|
||||
router.get('/postProcessing/:id', controllers.postProcessing)
|
||||
|
||||
|
||||
module.exports = router
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const app = require('./index.js')
|
||||
const port = process.env.PORT || '3000'
|
||||
app.listen(port, '0.0.0.0', () => {
|
||||
console.log('Server listening on port '+port);
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
const Agenda = require("agenda");
|
||||
const agenda = new Agenda({ db: { address: "mongodb://db:27017/agendaDb" } });
|
||||
const {Agent} = require('./agent');
|
||||
const agent = new Agent();
|
||||
|
||||
// Define a job
|
||||
agenda.define('followSale', async (job, done) => {
|
||||
const { saleId } = job.attrs.data;
|
||||
agent.followSale(saleId)
|
||||
.then(data => {;
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
agenda.define('prepareSale', async (job, done) => {
|
||||
const { saleId } = job.attrs.data;
|
||||
agent.prepareSale(saleId)
|
||||
.then(data => {;
|
||||
done();
|
||||
})
|
||||
});
|
||||
|
||||
module.exports = agenda;
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
const fetch = require('node-fetch');
|
||||
const { LotDb } = require("./lotDb");
|
||||
const lotDb = new LotDb();
|
||||
const { SaleDb } = require("./saleDb");
|
||||
const saleDb = new SaleDb();
|
||||
const moment = require('moment-timezone');
|
||||
|
||||
const Agent = class
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this.ApiAgentURL = "http://host.docker.internal:3020/api";
|
||||
}
|
||||
|
||||
async getSaleInfos(url)
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
url = encodeURIComponent(url);
|
||||
fetch(this.ApiAgentURL+'/sale/getSaleInfos/'+url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
resolve(data);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async prepareSale(id)
|
||||
{
|
||||
return new Promise(async (resolve, reject) => {
|
||||
|
||||
let Sale = await saleDb.get(id);
|
||||
|
||||
const DateSale = moment.tz(Sale.date, "Europe/Paris");
|
||||
const NowParis = moment.tz(new Date(),"Europe/Paris")
|
||||
|
||||
if (NowParis.isBefore(DateSale)){
|
||||
|
||||
let url = Sale.url
|
||||
url = encodeURIComponent(url);
|
||||
|
||||
fetch(this.ApiAgentURL+'/sale/getLotList/'+url)
|
||||
.then(response => response.json())
|
||||
.then( async data => {
|
||||
|
||||
for (let lot of data) {
|
||||
lot.sale_id = Sale._id
|
||||
await lotDb.post(lot);
|
||||
}
|
||||
resolve(data);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
}else{
|
||||
console.log("Sale started or finished");
|
||||
resolve([]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async followSale(id)
|
||||
{
|
||||
return new Promise(async (resolve, reject) => {
|
||||
let Sale = await saleDb.get(id);
|
||||
|
||||
let url = Sale.url
|
||||
url = encodeURIComponent(url);
|
||||
|
||||
fetch(this.ApiAgentURL+'/sale/followSale/'+url)
|
||||
.then(response => response.json())
|
||||
.then(async data => {
|
||||
|
||||
// set the Sale status to following
|
||||
Sale.status = "following";
|
||||
Sale = await saleDb.put(id, Sale);
|
||||
resolve(data);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = {Agent};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
const MongoClient = require("mongodb").MongoClient;
|
||||
const connectionString = "mongodb://db:27017";
|
||||
const client = new MongoClient(connectionString);
|
||||
|
||||
let db;
|
||||
|
||||
const connectDb = async () => {
|
||||
if (db) return db;
|
||||
try {
|
||||
const conn = await client.connect();
|
||||
db = conn.db("jucundus");
|
||||
return db;
|
||||
} catch(e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = connectDb;
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
|
||||
const connectDb = require("./db");
|
||||
|
||||
const save = async (newDocument) => {
|
||||
const db = await connectDb();
|
||||
if (!db) {
|
||||
throw new Error('Database not connected');
|
||||
}
|
||||
|
||||
const collection = db.collection("Favorites");
|
||||
let result = await collection.insertOne(newDocument);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const getAll = async () => {
|
||||
const db = await connectDb();
|
||||
if (!db) {
|
||||
throw new Error('Database not connected');
|
||||
}
|
||||
const collection = db.collection("Favorites");
|
||||
let result = await collection.find({}).toArray();
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
module.exports = { save, getAll };
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
|
||||
const { ObjectId } = require('mongodb');
|
||||
const connectDb = require("./db");
|
||||
|
||||
const LotDb = class
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this.getCollection();
|
||||
}
|
||||
|
||||
async getCollection()
|
||||
{
|
||||
const db = await connectDb();
|
||||
if (!db) {
|
||||
throw new Error('Database not connected');
|
||||
}
|
||||
this.collection = db.collection("Lots");
|
||||
}
|
||||
|
||||
// CRUD
|
||||
async get(id)
|
||||
{
|
||||
let result = await this.collection.findOne({_id: new ObjectId(id)});
|
||||
return result;
|
||||
}
|
||||
|
||||
async post(newDocument)
|
||||
{
|
||||
let result = await this.collection.insertOne(newDocument);
|
||||
return result;
|
||||
}
|
||||
|
||||
async put(id, data)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Fucntions
|
||||
|
||||
async getAll()
|
||||
{
|
||||
let result = await this.collection.find({}).toArray();
|
||||
return result;
|
||||
}
|
||||
|
||||
async getBySaleId(idSalePlatform, platformName)
|
||||
{
|
||||
console.log(platformName);
|
||||
let result = await this.collection.find({sale_id: new ObjectId(idSalePlatform), platform: platformName});
|
||||
return result.toArray();
|
||||
}
|
||||
|
||||
async getByIDPlatform(idLotPlatform, platformName)
|
||||
{
|
||||
let result = await this.collection.findOne({idPlatform: String(idLotPlatform), platform: platformName});
|
||||
return result;
|
||||
}
|
||||
|
||||
async deleteAllLotBySaleId(sale_id){
|
||||
let result = await this.collection.deleteMany({sale_id: new ObjectId(sale_id)});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
module.exports = {LotDb};
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
|
||||
const { ObjectId } = require('mongodb');
|
||||
const connectDb = require("./db");
|
||||
|
||||
const SaleDb = class
|
||||
{
|
||||
constructor()
|
||||
{
|
||||
this.getCollection();
|
||||
}
|
||||
|
||||
async getCollection()
|
||||
{
|
||||
const db = await connectDb();
|
||||
if (!db) {
|
||||
throw new Error('Database not connected');
|
||||
}
|
||||
this.collection = db.collection("Sales");
|
||||
}
|
||||
|
||||
// CRUD
|
||||
async get(id)
|
||||
{
|
||||
let result = await this.collection.findOne({_id: new ObjectId(id)});
|
||||
return result;
|
||||
}
|
||||
|
||||
async post(newDocument)
|
||||
{
|
||||
let result = await this.collection.insertOne(newDocument);
|
||||
return result;
|
||||
}
|
||||
|
||||
async put(id, data)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Fucntions
|
||||
|
||||
async getAll()
|
||||
{
|
||||
let result = await this.collection.find({}).toArray();
|
||||
return result;
|
||||
}
|
||||
|
||||
async getByUrl(url)
|
||||
{
|
||||
let result = await this.collection.findOne({url: url});
|
||||
return result;
|
||||
}
|
||||
|
||||
async getByIDPlatform(idSalePlatform, platformName)
|
||||
{
|
||||
let result = await this.collection.findOne({idPlatform: String(idSalePlatform), platform: String(platformName)});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SaleDb };
|
||||
Binary file not shown.
|
|
@ -0,0 +1,50 @@
|
|||
{
|
||||
"root": true,
|
||||
"ignorePatterns": [
|
||||
"projects/**/*"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
"files": [
|
||||
"*.ts"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"tsconfig.json"
|
||||
],
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.html"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:@angular-eslint/template/recommended"
|
||||
],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2022 Umut Esen
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
## Backend
|
||||
# Dev
|
||||
npm run dev
|
||||
http://localhost:3000
|
||||
|
||||
# Prod
|
||||
npm run start
|
||||
|
||||
## Frontend
|
||||
# Dev
|
||||
ng serve
|
||||
http://localhost:4200
|
||||
|
||||
## Docker Database Dev
|
||||
```bash
|
||||
docker-compose -f docker-compose-dev.yml build
|
||||
docker-compose -f docker-compose-dev.yml up
|
||||
```
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
[](https://dev.azure.com/umutesen/onthecode/_build/latest?definitionId=11)
|
||||
|
||||
# Angular Material Starter Template
|
||||
|
||||

|
||||
|
||||
|
||||
Angular Material Starter Template is a free template built with Angular and Angular Material. You can use it out of the box without having to change any file paths. Everything you need to start development on an Angular project is here.
|
||||
|
||||
Angular Material starter template has been built with the official style guide in mind, which means it promotes a clean folder structure and separation of concerns. The material template is fully responsive and contains the fundamental building blocks of a scalable Angular application:
|
||||
|
||||
Authentication module with login, logout and password reset components
|
||||
Responsive Admin dashboard with sidebar
|
||||
Account area with change password component
|
||||
All Angular Material components
|
||||
In addition to Angular, other well-known open-source libraries such as rxjs, moment and ngx-logger are also included.
|
||||
|
||||
This application template came as a result of several applications that I have developed over the past few years.
|
||||
|
||||
Having mostly used Angular Material component, I wanted to create a starter template to save time for greenfield projects. I developed it based on user feedback and it is a powerful Angular admin dashboard, which allows you to build products like admin panels, content management systems (CMS) and customer relationship management (CRM) software.
|
||||
|
||||
## Starter Template Features
|
||||
|
||||
Clean folder structure
|
||||
Core module
|
||||
Shared module
|
||||
Example feature modules
|
||||
Lazy-loaded feature modules
|
||||
Global error-handling
|
||||
Error logging with ngx-logger (logging to browser & remote API)
|
||||
HTTP Interceptors to inject JWT-tokens Authentication and role guards (for Role-based access)
|
||||
Shows spinner for all HTTP requests
|
||||
Angular flex layout
|
||||
Browser Support
|
||||
|
||||
At present, the template aims to support the last two versions of the following browsers:
|
||||
|
||||
Chrome
|
||||
Firefox
|
||||
Microsoft Edge
|
||||
Safari
|
||||
Opera
|
||||
|
||||
|
||||
## Development server
|
||||
|
||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
|
||||
|
||||
## Build
|
||||
|
||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities.
|
||||
|
||||
## Further help
|
||||
|
||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page.
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular-material-template": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:application": {
|
||||
"strict": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:browser",
|
||||
"options": {
|
||||
"outputPath": "dist/angular-material-template",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css",
|
||||
"node_modules/primeng/resources/themes/lara-light-blue/theme.css",
|
||||
"node_modules/primeng/resources/primeng.min.css"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "2mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true,
|
||||
"allowedCommonJsDependencies": ["moment-timezone"]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "angular-material-template:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular-material-template:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "angular-material-template:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-eslint/builder:lint",
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.html"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli": {
|
||||
"schematicCollections": [
|
||||
"@angular-eslint/schematics"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
basePath: '',
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
client: {
|
||||
jasmine: {
|
||||
// you can add configuration options for Jasmine here
|
||||
// the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
|
||||
// for example, you can disable the random execution with `random: false`
|
||||
// or set a specific seed with `seed: 4321`
|
||||
},
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
jasmineHtmlReporter: {
|
||||
suppressAll: true // removes the duplicated traces
|
||||
},
|
||||
coverageReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/angular-material-template'),
|
||||
subdir: '.',
|
||||
reporters: [
|
||||
{ type: 'html' },
|
||||
{ type: 'text-summary' }
|
||||
]
|
||||
},
|
||||
reporters: ['progress', 'kjhtml'],
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,57 @@
|
|||
{
|
||||
"name": "angular-material-template",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"lint": "ng lint",
|
||||
"test": "ng test",
|
||||
"build-production": "ng build --configuration=production --base-href=/angular-material-template/"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.2.2",
|
||||
"@angular/cdk": "^16.2.14",
|
||||
"@angular/common": "^17.2.2",
|
||||
"@angular/compiler": "^17.2.2",
|
||||
"@angular/core": "^17.2.2",
|
||||
"@angular/flex-layout": "^15.0.0-beta.42",
|
||||
"@angular/forms": "^17.2.2",
|
||||
"@angular/material": "^16.2.14",
|
||||
"@angular/material-moment-adapter": "^16.2.14",
|
||||
"@angular/platform-browser": "^17.2.2",
|
||||
"@angular/platform-browser-dynamic": "^17.2.2",
|
||||
"@angular/router": "^17.2.2",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.45",
|
||||
"ngx-logger": "^5.0.7",
|
||||
"primeng": "^17.8.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^17.2.1",
|
||||
"@angular-eslint/builder": "14.1.1",
|
||||
"@angular-eslint/eslint-plugin": "14.1.1",
|
||||
"@angular-eslint/eslint-plugin-template": "14.1.1",
|
||||
"@angular-eslint/template-parser": "14.1.1",
|
||||
"@angular/cli": "^17.2.1",
|
||||
"@angular/compiler-cli": "^17.2.2",
|
||||
"@types/jasmine": "~3.10.0",
|
||||
"@types/node": "^12.11.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
"eslint": "^8.23.0",
|
||||
"jasmine-core": "~4.0.0",
|
||||
"karma": "~6.3.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage": "~2.1.0",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "~1.7.0",
|
||||
"typescript": "~5.3.3"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,75 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { AuthGuard } from './core/guards/auth.guard';
|
||||
|
||||
const appRoutes: Routes = [
|
||||
{
|
||||
path: 'auth',
|
||||
loadChildren: () => import('./features/auth/auth.module').then(m => m.AuthModule),
|
||||
},
|
||||
{
|
||||
path: 'dashboard',
|
||||
loadChildren: () => import('./features/dashboard/dashboard.module').then(m => m.DashboardModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'sales',
|
||||
loadChildren: () => import('./features/sales/sales.module').then(m => m.SalesModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'favorites',
|
||||
loadChildren: () => import('./features/favorites/favorites.module').then(m => m.FavoritesModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'pictures',
|
||||
loadChildren: () => import('./features/pictures/pictures.module').then(m => m.PicturesModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'customers',
|
||||
loadChildren: () => import('./features/customers/customers.module').then(m => m.CustomersModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'users',
|
||||
loadChildren: () => import('./features/users/users.module').then(m => m.UsersModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'account',
|
||||
loadChildren: () => import('./features/account/account.module').then(m => m.AccountModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'icons',
|
||||
loadChildren: () => import('./features/icons/icons.module').then(m => m.IconsModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'typography',
|
||||
loadChildren: () => import('./features/typography/typography.module').then(m => m.TypographyModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: 'about',
|
||||
loadChildren: () => import('./features/about/about.module').then(m => m.AboutModule),
|
||||
canActivate: [AuthGuard]
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'dashboard',
|
||||
pathMatch: 'full'
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forRoot(appRoutes)
|
||||
],
|
||||
exports: [RouterModule],
|
||||
providers: []
|
||||
})
|
||||
export class AppRoutingModule { }
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
template: `<router-outlet></router-outlet>`
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
|
||||
import { AppComponent } from './app.component';
|
||||
import { CoreModule } from './core/core.module';
|
||||
import { SharedModule } from './shared/shared.module';
|
||||
import { CustomMaterialModule } from './custom-material/custom-material.module';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { LoggerModule } from 'ngx-logger';
|
||||
import { environment } from '../environments/environment';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
],
|
||||
imports: [
|
||||
BrowserModule,
|
||||
BrowserAnimationsModule,
|
||||
CoreModule,
|
||||
SharedModule,
|
||||
CustomMaterialModule.forRoot(),
|
||||
AppRoutingModule,
|
||||
LoggerModule.forRoot({
|
||||
serverLoggingUrl: `http://my-api/logs`,
|
||||
level: environment.logLevel,
|
||||
serverLogLevel: environment.serverLogLevel
|
||||
})
|
||||
],
|
||||
bootstrap: [AppComponent]
|
||||
})
|
||||
export class AppModule { }
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import { NgModule, Optional, SkipSelf, ErrorHandler } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
|
||||
import { MediaMatcher } from '@angular/cdk/layout';
|
||||
import { NGXLogger } from 'ngx-logger';
|
||||
|
||||
import { AuthInterceptor } from './interceptors/auth.interceptor';
|
||||
import { SpinnerInterceptor } from './interceptors/spinner.interceptor';
|
||||
import { AuthGuard } from './guards/auth.guard';
|
||||
import { throwIfAlreadyLoaded } from './guards/module-import.guard';
|
||||
import { GlobalErrorHandler } from './services/globar-error.handler';
|
||||
import { AdminGuard } from './guards/admin.guard';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
HttpClientModule
|
||||
],
|
||||
declarations: [
|
||||
],
|
||||
providers: [
|
||||
AuthGuard,
|
||||
AdminGuard,
|
||||
MediaMatcher,
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: SpinnerInterceptor,
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: HTTP_INTERCEPTORS,
|
||||
useClass: AuthInterceptor,
|
||||
multi: true
|
||||
},
|
||||
{
|
||||
provide: ErrorHandler,
|
||||
useClass: GlobalErrorHandler
|
||||
},
|
||||
{ provide: NGXLogger, useClass: NGXLogger },
|
||||
{ provide: 'LOCALSTORAGE', useValue: window.localStorage }
|
||||
],
|
||||
exports: [
|
||||
]
|
||||
})
|
||||
export class CoreModule {
|
||||
constructor(@Optional() @SkipSelf() parentModule: CoreModule) {
|
||||
throwIfAlreadyLoaded(parentModule, 'CoreModule');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
import { AdminGuard } from './admin.guard';
|
||||
|
||||
describe('AdminGuard', () => {
|
||||
|
||||
let router;
|
||||
let authService;
|
||||
|
||||
beforeEach(() => {
|
||||
router = jasmine.createSpyObj(['navigate']);
|
||||
authService = jasmine.createSpyObj(['getCurrentUser']);
|
||||
});
|
||||
|
||||
it('create an instance', () => {
|
||||
const guard = new AdminGuard(router, authService);
|
||||
expect(guard).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns true if user is admin', () => {
|
||||
const user = { 'isAdmin': true };
|
||||
authService.getCurrentUser.and.returnValue(user);
|
||||
const guard = new AdminGuard(router, authService);
|
||||
|
||||
const result = guard.canActivate();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if user does not exist', () => {
|
||||
authService.getCurrentUser.and.returnValue(null);
|
||||
const guard = new AdminGuard(router, authService);
|
||||
|
||||
const result = guard.canActivate();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if user is not admin', () => {
|
||||
const user = { 'isAdmin': false };
|
||||
authService.getCurrentUser.and.returnValue(user);
|
||||
const guard = new AdminGuard(router, authService);
|
||||
|
||||
const result = guard.canActivate();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('redirects to root if user is not an admin', () => {
|
||||
const user = { 'isAdmin': false };
|
||||
authService.getCurrentUser.and.returnValue(user);
|
||||
const guard = new AdminGuard(router, authService);
|
||||
|
||||
guard.canActivate();
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/']);
|
||||
});
|
||||
|
||||
it('redirects to root if user does not exist', () => {
|
||||
authService.getCurrentUser.and.returnValue(null);
|
||||
const guard = new AdminGuard(router, authService);
|
||||
|
||||
guard.canActivate();
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/']);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { AuthenticationService } from '../services/auth.service';
|
||||
|
||||
@Injectable()
|
||||
export class AdminGuard {
|
||||
|
||||
constructor(private router: Router,
|
||||
private authService: AuthenticationService) { }
|
||||
|
||||
canActivate() {
|
||||
const user = this.authService.getCurrentUser();
|
||||
|
||||
if (user && user.isAdmin) {
|
||||
return true;
|
||||
|
||||
} else {
|
||||
this.router.navigate(['/']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
import { AuthGuard } from './auth.guard';
|
||||
import * as moment from 'moment';
|
||||
|
||||
describe('AuthGuard', () => {
|
||||
|
||||
let router;
|
||||
let authService;
|
||||
let notificationService;
|
||||
|
||||
beforeEach(() => {
|
||||
router = jasmine.createSpyObj(['navigate']);
|
||||
authService = jasmine.createSpyObj(['getCurrentUser']);
|
||||
notificationService = jasmine.createSpyObj(['openSnackBar']);
|
||||
});
|
||||
|
||||
it('create an instance', () => {
|
||||
const guard = new AuthGuard(router, notificationService, authService);
|
||||
expect(guard).toBeTruthy();
|
||||
});
|
||||
|
||||
it('returns false if user is null', () => {
|
||||
authService.getCurrentUser.and.returnValue(null);
|
||||
const guard = new AuthGuard(router, notificationService, authService);
|
||||
|
||||
const result = guard.canActivate();
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('redirects to login if user is null', () => {
|
||||
authService.getCurrentUser.and.returnValue(null);
|
||||
const guard = new AuthGuard(router, notificationService, authService);
|
||||
|
||||
guard.canActivate();
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(['auth/login']);
|
||||
});
|
||||
|
||||
it('does not display expired notification if user is null', () => {
|
||||
authService.getCurrentUser.and.returnValue(null);
|
||||
const guard = new AuthGuard(router, notificationService, authService);
|
||||
|
||||
guard.canActivate();
|
||||
|
||||
expect(notificationService.openSnackBar).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('redirects to login if user session has expired', () => {
|
||||
const user = { expiration: moment().add(-1, 'minutes') };
|
||||
authService.getCurrentUser.and.returnValue(user);
|
||||
const guard = new AuthGuard(router, notificationService, authService);
|
||||
|
||||
guard.canActivate();
|
||||
|
||||
expect(router.navigate).toHaveBeenCalledWith(['auth/login']);
|
||||
});
|
||||
|
||||
it('displays notification if user session has expired', () => {
|
||||
const user = { expiration: moment().add(-1, 'seconds') };
|
||||
authService.getCurrentUser.and.returnValue(user);
|
||||
const guard = new AuthGuard(router, notificationService, authService);
|
||||
|
||||
guard.canActivate();
|
||||
|
||||
expect(notificationService.openSnackBar)
|
||||
.toHaveBeenCalledWith('Your session has expired');
|
||||
});
|
||||
|
||||
it('returns true if user session is valid', () => {
|
||||
const user = { expiration: moment().add(1, 'minutes') };
|
||||
authService.getCurrentUser.and.returnValue(user);
|
||||
const guard = new AuthGuard(router, notificationService, authService);
|
||||
|
||||
const result = guard.canActivate();
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import * as moment from 'moment';
|
||||
|
||||
import { AuthenticationService } from '../services/auth.service';
|
||||
import { NotificationService } from '../services/notification.service';
|
||||
|
||||
@Injectable()
|
||||
export class AuthGuard {
|
||||
|
||||
constructor(private router: Router,
|
||||
private notificationService: NotificationService,
|
||||
private authService: AuthenticationService) { }
|
||||
|
||||
canActivate() {
|
||||
const user = this.authService.getCurrentUser();
|
||||
|
||||
if (user && user.expiration) {
|
||||
|
||||
if (moment() < moment(user.expiration)) {
|
||||
return true;
|
||||
} else {
|
||||
this.notificationService.openSnackBar('Your session has expired');
|
||||
this.router.navigate(['auth/login']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.router.navigate(['auth/login']);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) {
|
||||
if (parentModule) {
|
||||
throw new Error(`${moduleName} has already been loaded. Import Core modules in the AppModule only.`);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpInterceptor, HttpErrorResponse } from '@angular/common/http';
|
||||
import { HttpRequest } from '@angular/common/http';
|
||||
import { HttpHandler } from '@angular/common/http';
|
||||
import { HttpEvent } from '@angular/common/http';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
import { AuthenticationService } from '../services/auth.service';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
||||
@Injectable()
|
||||
export class AuthInterceptor implements HttpInterceptor {
|
||||
|
||||
constructor(private authService: AuthenticationService,
|
||||
private router: Router,
|
||||
private dialog: MatDialog) { }
|
||||
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
|
||||
const user = this.authService.getCurrentUser();
|
||||
|
||||
if (user && user.token) {
|
||||
|
||||
const cloned = req.clone({
|
||||
headers: req.headers.set('Authorization',
|
||||
'Bearer ' + user.token)
|
||||
});
|
||||
|
||||
return next.handle(cloned).pipe(tap(() => { }, (err: any) => {
|
||||
if (err instanceof HttpErrorResponse) {
|
||||
if (err.status === 401) {
|
||||
this.dialog.closeAll();
|
||||
this.router.navigate(['/auth/login']);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
} else {
|
||||
return next.handle(req);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpInterceptor, HttpResponse } from '@angular/common/http';
|
||||
import { HttpRequest } from '@angular/common/http';
|
||||
import { HttpHandler } from '@angular/common/http';
|
||||
import { HttpEvent } from '@angular/common/http';
|
||||
import { tap } from 'rxjs/operators';
|
||||
|
||||
import { SpinnerService } from './../services/spinner.service';
|
||||
|
||||
@Injectable()
|
||||
export class SpinnerInterceptor implements HttpInterceptor {
|
||||
|
||||
constructor(private spinnerService: SpinnerService) { }
|
||||
|
||||
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
|
||||
|
||||
this.spinnerService.show();
|
||||
|
||||
return next
|
||||
.handle(req)
|
||||
.pipe(
|
||||
tap((event: HttpEvent<any>) => {
|
||||
if (event instanceof HttpResponse) {
|
||||
this.spinnerService.hide();
|
||||
}
|
||||
}, (error) => {
|
||||
this.spinnerService.hide();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import { Injectable, Inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { LotInfo } from './model/lotInfo.interface';
|
||||
import { SaleInfo } from './model/saleInfo.interface';
|
||||
import { Sale } from './model/sale.interface';
|
||||
import { Lot } from './model/lot.interface';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class apiService {
|
||||
ApiURL = "http://localhost:3000/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);
|
||||
}
|
||||
|
||||
// Sale
|
||||
getSaleInfos(url: string): Observable<SaleInfo> {
|
||||
let encodeUrl = encodeURIComponent(url);
|
||||
|
||||
return this.http.get<SaleInfo>(this.ApiURL+'/sale/getSaleInfos/'+encodeUrl);
|
||||
}
|
||||
|
||||
prepareSale(saleInfo: SaleInfo): Observable<any> {
|
||||
|
||||
let follow = this.http.get<any>(this.ApiURL+'/sale/prepareSale/'+saleInfo._id);
|
||||
return follow
|
||||
}
|
||||
|
||||
followSale(saleInfo: SaleInfo): Observable<any> {
|
||||
|
||||
let follow = this.http.get<any>(this.ApiURL+'/sale/followSale/'+saleInfo._id);
|
||||
return follow
|
||||
}
|
||||
|
||||
// CRUD DB Sale
|
||||
getSale(_id: String): Observable<Sale> {
|
||||
return this.http.get<Sale>(this.ApiURL+'/sale/sale/'+_id);
|
||||
}
|
||||
|
||||
saveSale(saleInfo: SaleInfo): Observable<SaleInfo> {
|
||||
return this.http.post<SaleInfo>(this.ApiURL+'/sale/sale', saleInfo);
|
||||
}
|
||||
|
||||
updateSale(saleInfo: SaleInfo): Observable<SaleInfo> {
|
||||
return this.http.put<SaleInfo>(this.ApiURL+'/sale/sale/'+saleInfo._id, saleInfo);
|
||||
}
|
||||
|
||||
deleteSale(_id: String): Observable<any> {
|
||||
return this.http.delete<any>(this.ApiURL+'/sale/sale/'+_id);
|
||||
}
|
||||
|
||||
// Function DB Sale
|
||||
|
||||
getAllSale(): Observable<SaleInfo[]> {
|
||||
return this.http.get<SaleInfo[]>(this.ApiURL+'/sale/getAll');
|
||||
}
|
||||
|
||||
postProcessing(_id: String): Observable<any> {
|
||||
return this.http.get<any>(this.ApiURL+'/sale/postProcessing/'+_id);
|
||||
}
|
||||
|
||||
|
||||
//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');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
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 * as moment from 'moment';
|
||||
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { of, EMPTY } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthenticationService {
|
||||
|
||||
constructor(private http: HttpClient,
|
||||
@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;
|
||||
}));
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
// clear token remove user from local storage to log user out
|
||||
this.localStorage.removeItem('currentUser');
|
||||
}
|
||||
|
||||
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'
|
||||
};
|
||||
}
|
||||
|
||||
passwordResetRequest(email: string) {
|
||||
return of(true).pipe(delay(1000));
|
||||
}
|
||||
|
||||
changePassword(email: string, currentPwd: string, newPwd: string) {
|
||||
return of(true).pipe(delay(1000));
|
||||
}
|
||||
|
||||
passwordReset(email: string, token: string, password: string, confirmPassword: string): any {
|
||||
return of(true).pipe(delay(1000));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { ErrorHandler, Injectable, Injector } from '@angular/core';
|
||||
import { NGXLogger } from 'ngx-logger';
|
||||
|
||||
@Injectable()
|
||||
export class GlobalErrorHandler implements ErrorHandler {
|
||||
|
||||
|
||||
constructor(private injector: Injector) { }
|
||||
|
||||
handleError(error: Error) {
|
||||
// Obtain dependencies at the time of the error
|
||||
// This is because the GlobalErrorHandler is registered first
|
||||
// which prevents constructor dependency injection
|
||||
const logger = this.injector.get(NGXLogger);
|
||||
|
||||
const err = {
|
||||
message: error.message ? error.message : error.toString(),
|
||||
stack: error.stack ? error.stack : ''
|
||||
};
|
||||
|
||||
// Log the error
|
||||
logger.error(err);
|
||||
|
||||
// Re-throw the error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
|
||||
export interface Bid {
|
||||
timestamp: string;
|
||||
amount: number;
|
||||
auctioned_type: string;
|
||||
}
|
||||
|
||||
export interface Auctioned {
|
||||
timestamp: string;
|
||||
amount: number;
|
||||
auctioned_type: string;
|
||||
sold: boolean;
|
||||
}
|
||||
|
||||
export interface Lot {
|
||||
_id: {
|
||||
$oid: string;
|
||||
};
|
||||
idPlatform: string;
|
||||
platform: string;
|
||||
timestamp: string;
|
||||
lotNumber: string;
|
||||
RawData?: Object;
|
||||
sale_id: {
|
||||
$oid: string;
|
||||
};
|
||||
Bids?: Bid[];
|
||||
auctioned?: Auctioned;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
export interface LotInfo {
|
||||
idLotInterencheres: string;
|
||||
url: string;
|
||||
title: string;
|
||||
lotNumber: string;
|
||||
EstimateLow: number;
|
||||
EstimateHigh: number;
|
||||
Description: string;
|
||||
feesText: string;
|
||||
fees: string;
|
||||
saleInfo: {
|
||||
idSaleInterencheres: string;
|
||||
url: string;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
|
||||
export interface PostProcessing {
|
||||
nbrLots: number;
|
||||
duration: number;
|
||||
durationPerLots: string;
|
||||
totalAmount: number;
|
||||
averageAmount: string;
|
||||
medianAmount: string;
|
||||
}
|
||||
|
||||
export interface Sale {
|
||||
_id: {
|
||||
$oid: string;
|
||||
};
|
||||
idPlatform: string;
|
||||
platform: string;
|
||||
url: string;
|
||||
title: string;
|
||||
date: string;
|
||||
location: string;
|
||||
saleHouseName: string;
|
||||
status: string;
|
||||
postProcessing?: PostProcessing;
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export interface SaleInfo {
|
||||
_id: string;
|
||||
idPlatform: string;
|
||||
platform: string;
|
||||
url: string;
|
||||
title: string;
|
||||
date: string;
|
||||
location: string;
|
||||
saleHouseName: string;
|
||||
status: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class NotificationService {
|
||||
|
||||
constructor(private snackBar: MatSnackBar) { }
|
||||
|
||||
public openSnackBar(message: string) {
|
||||
this.snackBar.open(message, '', {
|
||||
duration: 5000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import { SpinnerConsumer } from '../../shared/mocks/spinner-consumer';
|
||||
import { SpinnerService } from './spinner.service';
|
||||
|
||||
describe('BusyIndicatorService', () => {
|
||||
let component: SpinnerService;
|
||||
let consumer1: SpinnerConsumer;
|
||||
let consumer2: SpinnerConsumer;
|
||||
|
||||
beforeEach(() => {
|
||||
component = new SpinnerService();
|
||||
consumer1 = new SpinnerConsumer(component);
|
||||
consumer2 = new SpinnerConsumer(component);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should initialise visibility to false', () => {
|
||||
component.visibility.subscribe((value: boolean) => {
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should broadcast visibility to all consumers', () => {
|
||||
expect(consumer1.isBusy).toBe(false);
|
||||
expect(consumer2.isBusy).toBe(false);
|
||||
});
|
||||
|
||||
it('should broadcast visibility to all consumers when the value changes', () => {
|
||||
component.visibility.next(true);
|
||||
|
||||
expect(consumer1.isBusy).toBe(true);
|
||||
expect(consumer2.isBusy).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class SpinnerService {
|
||||
|
||||
visibility = new BehaviorSubject(false);
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
show() {
|
||||
this.visibility.next(true);
|
||||
}
|
||||
|
||||
hide() {
|
||||
this.visibility.next(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import { NgModule, LOCALE_ID } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { MatMomentDateModule, MomentDateAdapter, MAT_MOMENT_DATE_FORMATS } from '@angular/material-moment-adapter';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatFormFieldModule} from '@angular/material/form-field';
|
||||
import {MatRadioModule} from '@angular/material/radio';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import {MatSliderModule} from '@angular/material/slider';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatBadgeModule } from '@angular/material/badge';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatGridListModule } from '@angular/material/grid-list';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import {MatTabsModule} from '@angular/material/tabs';
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatChipsModule } from '@angular/material/chips';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import {MatProgressBarModule} from '@angular/material/progress-bar';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { SelectCheckAllComponent } from './select-check-all/select-check-all.component';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
|
||||
|
||||
|
||||
// Prime NG
|
||||
import { GalleriaModule } from 'primeng/galleria';
|
||||
import { ImageModule } from 'primeng/image';
|
||||
import { SkeletonModule } from 'primeng/skeleton';
|
||||
|
||||
export const MY_FORMATS = {
|
||||
parse: {
|
||||
dateInput: 'DD MMM YYYY',
|
||||
},
|
||||
display: {
|
||||
dateInput: 'DD MMM YYYY',
|
||||
monthYearLabel: 'MMM YYYY',
|
||||
dateA11yLabel: 'LL',
|
||||
monthYearA11yLabel: 'MMMM YYYY'
|
||||
}
|
||||
};
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatMomentDateModule,
|
||||
MatSidenavModule, MatIconModule, MatToolbarModule, MatButtonModule,
|
||||
MatListModule, MatGridListModule, MatCardModule, MatProgressBarModule, MatInputModule,
|
||||
MatSnackBarModule, MatProgressSpinnerModule, MatDatepickerModule,
|
||||
MatAutocompleteModule, MatTableModule, MatDialogModule, MatTabsModule,
|
||||
MatTooltipModule, MatSelectModule, MatPaginatorModule, MatChipsModule,
|
||||
MatButtonToggleModule, MatSlideToggleModule, MatBadgeModule, MatCheckboxModule,
|
||||
MatExpansionModule, DragDropModule, MatSortModule,
|
||||
|
||||
GalleriaModule,ImageModule,SkeletonModule
|
||||
],
|
||||
exports: [
|
||||
CommonModule,
|
||||
MatSidenavModule, MatIconModule, MatToolbarModule, MatButtonModule,
|
||||
MatListModule, MatGridListModule, MatCardModule, MatProgressBarModule, MatInputModule,
|
||||
MatSnackBarModule, MatMenuModule, MatProgressSpinnerModule, MatDatepickerModule,
|
||||
MatAutocompleteModule, MatTableModule, MatDialogModule, MatTabsModule,
|
||||
MatTooltipModule, MatSelectModule, MatPaginatorModule, MatChipsModule,
|
||||
MatButtonToggleModule, MatSlideToggleModule, MatBadgeModule, MatCheckboxModule,
|
||||
MatExpansionModule, SelectCheckAllComponent, DragDropModule, MatSortModule,
|
||||
|
||||
GalleriaModule,ImageModule,SkeletonModule
|
||||
],
|
||||
providers: [
|
||||
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE] },
|
||||
{ provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
|
||||
{ provide: LOCALE_ID, useValue: 'en-gb' }
|
||||
],
|
||||
declarations: [SelectCheckAllComponent]
|
||||
})
|
||||
export class CustomMaterialModule {
|
||||
static forRoot() {
|
||||
return {
|
||||
ngModule: CustomMaterialModule,
|
||||
providers: [
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
app-select-check-all .mat-checkbox-layout,
|
||||
app-select-check-all .mat-checkbox-label {
|
||||
width:100% !important;
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<mat-checkbox class="mat-option" [indeterminate]="isIndeterminate()" [checked]="isChecked()" (click)="$event.stopPropagation()"
|
||||
(change)="toggleSelection($event)">
|
||||
{{text}}
|
||||
</mat-checkbox>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { Component, Input, ViewEncapsulation } from '@angular/core';
|
||||
import { UntypedFormControl } from '@angular/forms';
|
||||
import { MatCheckboxChange } from '@angular/material/checkbox';
|
||||
@Component({
|
||||
selector: 'app-select-check-all',
|
||||
templateUrl: './select-check-all.component.html',
|
||||
styleUrls: ['./select-check-all.component.css'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SelectCheckAllComponent {
|
||||
@Input()
|
||||
model: UntypedFormControl = new UntypedFormControl;
|
||||
@Input() values = [];
|
||||
@Input() text = 'Select All';
|
||||
|
||||
constructor() { }
|
||||
|
||||
isChecked(): boolean {
|
||||
return this.model.value && this.values.length
|
||||
&& this.model.value.length === this.values.length;
|
||||
}
|
||||
|
||||
isIndeterminate(): boolean {
|
||||
return this.model.value && this.values.length && this.model.value.length
|
||||
&& this.model.value.length < this.values.length;
|
||||
}
|
||||
|
||||
toggleSelection(change: MatCheckboxChange): void {
|
||||
if (change.checked) {
|
||||
this.model.setValue(this.values);
|
||||
} else {
|
||||
this.model.setValue([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
mat-icon {
|
||||
color: rgb(200, 0, 0);
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<div class="container" fxLayout="row" fxLayoutAlign="center none">
|
||||
<div fxFlex="95%">
|
||||
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<h2>About</h2>
|
||||
|
||||
<p>
|
||||
Built on top of <a rel="noreferrer noopener" aria-label="Angular (opens in a new tab)"
|
||||
href="http://angular.io" target="_blank">Angular</a> & <a rel="noreferrer noopener"
|
||||
aria-label="Angular Material (opens in a new tab)" href="http://material.angular.io"
|
||||
target="_blank">Angular Material</a>, angular-material-template provides a simple template that you can use for your next project.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Support the project by starring it on <a href="https://github.com/umutesen/angular-material-template"
|
||||
target="_blank">
|
||||
GitHub
|
||||
</a>.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Made with <mat-icon>favorite</mat-icon> by <a href="https://onthecode.co.uk" target="_blank"
|
||||
aria-label="onthecode (opens in a new tab)">onthecode</a>.
|
||||
</p>
|
||||
|
||||
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AboutPageComponent } from './about-page.component';
|
||||
|
||||
describe('AboutHomeComponent', () => {
|
||||
let component: AboutPageComponent;
|
||||
let fixture: ComponentFixture<AboutPageComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ AboutPageComponent ]
|
||||
})
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AboutPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-about-page',
|
||||
templateUrl: './about-page.component.html',
|
||||
styleUrls: ['./about-page.component.css']
|
||||
})
|
||||
export class AboutPageComponent {
|
||||
|
||||
constructor() { }
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { LayoutComponent } from '../../shared/layout/layout.component';
|
||||
import { AboutPageComponent } from './about-page/about-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LayoutComponent,
|
||||
children: [
|
||||
{ path: '', component: AboutPageComponent },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AboutRoutingModule { }
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { AboutRoutingModule } from './about-routing.module';
|
||||
import { AboutPageComponent } from './about-page/about-page.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [AboutPageComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
AboutRoutingModule
|
||||
]
|
||||
})
|
||||
export class AboutModule { }
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<div class="container" fxLayout="row" fxLayoutAlign="center none">
|
||||
|
||||
<div fxFlex="95%">
|
||||
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<h2>My Profile</h2>
|
||||
|
||||
<div fxLayout="row" fxLayout.sm="column" fxLayout.xs="column">
|
||||
|
||||
<div fxFlex="30%" fxFlex.sm="95%" fxFlex.xs="95%">
|
||||
<app-profile-details></app-profile-details>
|
||||
</div>
|
||||
|
||||
<div fxFlex></div>
|
||||
|
||||
<div fxFlex="65%" fxFlex.sm="95%" fxFlex.xs="950%">
|
||||
|
||||
|
||||
<mat-tab-group>
|
||||
<mat-tab label="Change Password">
|
||||
<app-change-password></app-change-password>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
@Component({
|
||||
selector: 'app-account-page',
|
||||
templateUrl: './account-page.component.html',
|
||||
styleUrls: ['./account-page.component.css']
|
||||
})
|
||||
export class AccountPageComponent implements OnInit {
|
||||
|
||||
constructor(private titleService: Title) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.titleService.setTitle('Jucundus - Account');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { LayoutComponent } from 'src/app/shared/layout/layout.component';
|
||||
|
||||
import { AccountPageComponent } from './account-page/account-page.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LayoutComponent,
|
||||
children: [
|
||||
{ path: 'profile', component: AccountPageComponent },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AccountRoutingModule { }
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { AccountRoutingModule } from './account-routing.module';
|
||||
import { AccountPageComponent } from './account-page/account-page.component';
|
||||
import { ChangePasswordComponent } from './change-password/change-password.component';
|
||||
import { ProfileDetailsComponent } from './profile-details/profile-details.component';
|
||||
import { SharedModule } from 'src/app/shared/shared.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
AccountRoutingModule
|
||||
],
|
||||
declarations: [AccountPageComponent, ChangePasswordComponent, ProfileDetailsComponent],
|
||||
exports: [AccountPageComponent]
|
||||
})
|
||||
export class AccountModule { }
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
.password-rules .mat-divider {
|
||||
position: unset !important;
|
||||
}
|
||||
.container{
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<form [formGroup]="form">
|
||||
<p>Use the form below to change your password.</p>
|
||||
|
||||
<div fxLayout="row">
|
||||
|
||||
<div fxFlex="40%" fxFlex.md="60%" fxFlex.sm="50%" fxFlex.xs="100%">
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<input matInput placeholder="Current Password" formControlName="currentPassword" [type]="hideCurrentPassword ? 'password' : 'text'"
|
||||
autocomplete="current-password">
|
||||
<mat-icon matSuffix (click)="hideCurrentPassword = !hideCurrentPassword">
|
||||
{{hideCurrentPassword ? 'visibility' : 'visibility_off'}}
|
||||
</mat-icon>
|
||||
|
||||
<mat-error *ngIf="form.controls['currentPassword'].hasError('required')">
|
||||
Please enter a your current password
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<input matInput placeholder="New Password" formControlName="newPassword" [type]="hideNewPassword ? 'password' : 'text'" autocomplete="new-password">
|
||||
<mat-icon matSuffix (click)="hideNewPassword = !hideNewPassword">
|
||||
{{hideNewPassword ? 'visibility' : 'visibility_off'}}
|
||||
</mat-icon>
|
||||
|
||||
<mat-error *ngIf="form.controls['newPassword'].hasError('required')">
|
||||
Please enter a new password
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<input matInput placeholder="Confirm New Password" formControlName="newPasswordConfirm" [type]="hideNewPassword ? 'password' : 'text'"
|
||||
autocomplete="new-password">
|
||||
<mat-icon matSuffix (click)="hideNewPassword = !hideNewPassword">
|
||||
{{hideNewPassword ? 'visibility' : 'visibility_off'}}
|
||||
</mat-icon>
|
||||
|
||||
<mat-error *ngIf="form.controls['newPasswordConfirm'].hasError('required')">
|
||||
Please confirm your new password
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<button mat-raised-button color="primary" [disabled]="form.invalid || disableSubmit" (click)="changePassword()">Save</button>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<!-- <div class="password-rules" fxFlex="65%" fxFlex.sm="90%" fxFlex.xs="95%">
|
||||
Password rules:
|
||||
<mat-list>
|
||||
<mat-list-item>
|
||||
Must be at least 6 characters
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item>
|
||||
Must contain at least one non alphanumeric character
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item>
|
||||
Must contain at least one lowercase ('a'-'z')
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item>
|
||||
Must contain at least one uppercase ('A'-'Z')
|
||||
</mat-list-item>
|
||||
</mat-list>
|
||||
</div> -->
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { NGXLogger } from 'ngx-logger';
|
||||
import { AuthenticationService } from 'src/app/core/services/auth.service';
|
||||
import { NotificationService } from 'src/app/core/services/notification.service';
|
||||
import { SpinnerService } from 'src/app/core/services/spinner.service';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-password',
|
||||
templateUrl: './change-password.component.html',
|
||||
styleUrls: ['./change-password.component.css']
|
||||
})
|
||||
export class ChangePasswordComponent implements OnInit {
|
||||
|
||||
form!: UntypedFormGroup;
|
||||
hideCurrentPassword: boolean;
|
||||
hideNewPassword: boolean;
|
||||
currentPassword!: string;
|
||||
newPassword!: string;
|
||||
newPasswordConfirm!: string;
|
||||
disableSubmit!: boolean;
|
||||
|
||||
constructor(private authService: AuthenticationService,
|
||||
private logger: NGXLogger,
|
||||
private spinnerService: SpinnerService,
|
||||
private notificationService: NotificationService) {
|
||||
|
||||
this.hideCurrentPassword = true;
|
||||
this.hideNewPassword = true;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.form = new UntypedFormGroup({
|
||||
currentPassword: new UntypedFormControl('', Validators.required),
|
||||
newPassword: new UntypedFormControl('', Validators.required),
|
||||
newPasswordConfirm: new UntypedFormControl('', Validators.required),
|
||||
});
|
||||
|
||||
this.form.get('currentPassword')?.valueChanges
|
||||
.subscribe(val => { this.currentPassword = val; });
|
||||
|
||||
this.form.get('newPassword')?.valueChanges
|
||||
.subscribe(val => { this.newPassword = val; });
|
||||
|
||||
this.form.get('newPasswordConfirm')?.valueChanges
|
||||
.subscribe(val => { this.newPasswordConfirm = val; });
|
||||
|
||||
this.spinnerService.visibility.subscribe((value) => {
|
||||
this.disableSubmit = value;
|
||||
});
|
||||
}
|
||||
|
||||
changePassword() {
|
||||
|
||||
if (this.newPassword !== this.newPasswordConfirm) {
|
||||
this.notificationService.openSnackBar('New passwords do not match.');
|
||||
return;
|
||||
}
|
||||
|
||||
const email = this.authService.getCurrentUser().email;
|
||||
|
||||
this.authService.changePassword(email, this.currentPassword, this.newPassword)
|
||||
.subscribe(
|
||||
data => {
|
||||
this.logger.info(`User ${email} changed password.`);
|
||||
this.form.reset();
|
||||
this.notificationService.openSnackBar('Your password has been changed.');
|
||||
},
|
||||
error => {
|
||||
this.notificationService.openSnackBar(error.error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.profile-card {
|
||||
text-align: center;
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<div class="profile-card">
|
||||
<img src="assets/images/user.png" [alt]="fullName">
|
||||
|
||||
<h2 class="title">
|
||||
{{fullName}}
|
||||
</h2>
|
||||
|
||||
<label>
|
||||
{{alias}}
|
||||
</label>
|
||||
|
||||
<label>
|
||||
{{email}}
|
||||
</label>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { AuthenticationService } from 'src/app/core/services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-profile-details',
|
||||
templateUrl: './profile-details.component.html',
|
||||
styleUrls: ['./profile-details.component.css']
|
||||
})
|
||||
export class ProfileDetailsComponent implements OnInit {
|
||||
|
||||
fullName: string = "";
|
||||
email: string = "";
|
||||
alias: string = "";
|
||||
|
||||
constructor(private authService: AuthenticationService) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.fullName = this.authService.getCurrentUser().fullName;
|
||||
this.email = this.authService.getCurrentUser().email;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { PasswordResetRequestComponent } from './password-reset-request/password-reset-request.component';
|
||||
import { PasswordResetComponent } from './password-reset/password-reset.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{ path: 'login', component: LoginComponent },
|
||||
{ path: 'password-reset-request', component: PasswordResetRequestComponent },
|
||||
{ path: 'password-reset', component: PasswordResetComponent }
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class AuthRoutingModule { }
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { AuthRoutingModule } from './auth-routing.module';
|
||||
import { LoginComponent } from './login/login.component';
|
||||
import { PasswordResetRequestComponent } from './password-reset-request/password-reset-request.component';
|
||||
import { PasswordResetComponent } from './password-reset/password-reset.component';
|
||||
import { SharedModule } from 'src/app/shared/shared.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
AuthRoutingModule
|
||||
],
|
||||
declarations: [LoginComponent, PasswordResetRequestComponent, PasswordResetComponent]
|
||||
})
|
||||
export class AuthModule { }
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
<div class="container login-container" fxLayout="row" fxLayoutAlign="center center">
|
||||
<form [formGroup]="loginForm" fxFlex="30%" fxFlex.sm="50%" fxFlex.xs="90%">
|
||||
<mat-card>
|
||||
<mat-card-title>Jucundus</mat-card-title>
|
||||
<mat-card-subtitle>Log in to your account</mat-card-subtitle>
|
||||
<mat-card-content>
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<input id="emailInput" matInput placeholder="Email" formControlName="email" autocomplete="email"
|
||||
type="email">
|
||||
|
||||
<mat-error id="invalidEmailError" *ngIf="loginForm.controls['email'].hasError('email')">
|
||||
Please enter a valid email address
|
||||
</mat-error>
|
||||
<mat-error id="requiredEmailError" *ngIf="loginForm.controls['email'].hasError('required')">
|
||||
Email is
|
||||
<strong>required</strong>
|
||||
</mat-error>
|
||||
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<input id="passwordInput" matInput placeholder="Password" formControlName="password" type="password"
|
||||
autocomplete="current-password">
|
||||
<mat-error id="requiredPasswordError" *ngIf="loginForm.controls['email'].hasError('required')">
|
||||
Password is
|
||||
<strong>required</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<div class="full-width">
|
||||
<mat-slide-toggle formControlName="rememberMe">Remember my email address</mat-slide-toggle>
|
||||
</div>
|
||||
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions class="login-actions">
|
||||
<button mat-raised-button id="login" color="primary" [disabled]="loginForm.invalid || loading"
|
||||
(click)="login()">Login</button>
|
||||
<button mat-button id="resetPassword" (click)="resetPassword()" type="button">Reset Password</button>
|
||||
</mat-card-actions>
|
||||
|
||||
</mat-card>
|
||||
|
||||
<mat-progress-bar *ngIf="loading" mode="indeterminate"></mat-progress-bar>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { UntypedFormControl, Validators, UntypedFormGroup } from '@angular/forms';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { AuthenticationService } from 'src/app/core/services/auth.service';
|
||||
import { NotificationService } from 'src/app/core/services/notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
templateUrl: './login.component.html',
|
||||
styleUrls: ['./login.component.css']
|
||||
})
|
||||
export class LoginComponent implements OnInit {
|
||||
|
||||
loginForm!: UntypedFormGroup;
|
||||
loading!: boolean;
|
||||
|
||||
constructor(private router: Router,
|
||||
private titleService: Title,
|
||||
private notificationService: NotificationService,
|
||||
private authenticationService: AuthenticationService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.titleService.setTitle('Jucundus - Login');
|
||||
this.authenticationService.logout();
|
||||
this.createForm();
|
||||
}
|
||||
|
||||
private createForm() {
|
||||
const savedUserEmail = localStorage.getItem('savedUserEmail');
|
||||
|
||||
this.loginForm = new UntypedFormGroup({
|
||||
email: new UntypedFormControl(savedUserEmail, [Validators.required, Validators.email]),
|
||||
password: new UntypedFormControl('', Validators.required),
|
||||
rememberMe: new UntypedFormControl(savedUserEmail !== null)
|
||||
});
|
||||
}
|
||||
|
||||
login() {
|
||||
const email = this.loginForm.get('email')?.value;
|
||||
const password = this.loginForm.get('password')?.value;
|
||||
const rememberMe = this.loginForm.get('rememberMe')?.value;
|
||||
|
||||
this.loading = true;
|
||||
this.authenticationService
|
||||
.login(email.toLowerCase(), password)
|
||||
.subscribe(
|
||||
data => {
|
||||
if (rememberMe) {
|
||||
localStorage.setItem('savedUserEmail', email);
|
||||
} else {
|
||||
localStorage.removeItem('savedUserEmail');
|
||||
}
|
||||
this.router.navigate(['/']);
|
||||
},
|
||||
error => {
|
||||
this.notificationService.openSnackBar(error.error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
resetPassword() {
|
||||
this.router.navigate(['/auth/password-reset-request']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<div class="container login-container" fxLayout="row" fxLayoutAlign="center center">
|
||||
<form [formGroup]="form" fxFlex="30%" fxFlex.sm="50%" fxFlex.xs="90%">
|
||||
<mat-card>
|
||||
<mat-card-title>Jucundus</mat-card-title>
|
||||
<mat-card-subtitle>Reset your password</mat-card-subtitle>
|
||||
<mat-card-content>
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<input id="emailInput" matInput placeholder="Email" formControlName="email" autocomplete="email" type="email">
|
||||
|
||||
<mat-error id="invalidEmailError" *ngIf="form.controls['email'].hasError('email')">
|
||||
Please enter a valid email address
|
||||
</mat-error>
|
||||
<mat-error id="requiredEmailError" *ngIf="form.controls['email'].hasError('required')">
|
||||
Email is
|
||||
<strong>required</strong>
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions class="login-actions">
|
||||
<button id="submit" mat-raised-button color="primary" [disabled]="form.invalid || loading"
|
||||
(click)="resetPassword()">Reset Password</button>
|
||||
<button id="cancel" mat-button (click)="cancel()">Cancel</button>
|
||||
</mat-card-actions>
|
||||
|
||||
</mat-card>
|
||||
|
||||
<mat-progress-bar *ngIf="loading" mode="indeterminate"></mat-progress-bar>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import { Router } from '@angular/router';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
|
||||
import { NotificationService } from 'src/app/core/services/notification.service';
|
||||
import { AuthenticationService } from 'src/app/core/services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-password-reset-request',
|
||||
templateUrl: './password-reset-request.component.html',
|
||||
styleUrls: ['./password-reset-request.component.css']
|
||||
})
|
||||
export class PasswordResetRequestComponent implements OnInit {
|
||||
|
||||
private email!: string;
|
||||
form!: UntypedFormGroup;
|
||||
loading!: boolean;
|
||||
|
||||
constructor(private authService: AuthenticationService,
|
||||
private notificationService: NotificationService,
|
||||
private titleService: Title,
|
||||
private router: Router) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.titleService.setTitle('Jucundus - Password Reset Request');
|
||||
|
||||
this.form = new UntypedFormGroup({
|
||||
email: new UntypedFormControl('', [Validators.required, Validators.email])
|
||||
});
|
||||
|
||||
this.form.get('email')?.valueChanges
|
||||
.subscribe((val: string) => { this.email = val.toLowerCase(); });
|
||||
}
|
||||
|
||||
resetPassword() {
|
||||
this.loading = true;
|
||||
this.authService.passwordResetRequest(this.email)
|
||||
.subscribe(
|
||||
results => {
|
||||
this.router.navigate(['/auth/login']);
|
||||
this.notificationService.openSnackBar('Password verification mail has been sent to your email address.');
|
||||
},
|
||||
error => {
|
||||
this.loading = false;
|
||||
this.notificationService.openSnackBar(error.error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<div class="container login-container" fxLayout="row" fxLayoutAlign="center center">
|
||||
<form [formGroup]="form" fxFlex="30%" fxFlex.sm="50%" fxFlex.xs="90%">
|
||||
<mat-card>
|
||||
<mat-card-title>Jucundus</mat-card-title>
|
||||
<mat-card-subtitle>Reset your password</mat-card-subtitle>
|
||||
<mat-card-content>
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<input id="emailInput" matInput readonly disabled [value]="email">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<input id="passwordInput" matInput placeholder="New Password" formControlName="newPassword" [type]="hideNewPassword ? 'password' : 'text'" autocomplete="new-password">
|
||||
<mat-icon id="togglePasswordVisibility" matSuffix (click)="hideNewPassword = !hideNewPassword">
|
||||
{{hideNewPassword ? 'visibility' : 'visibility_off'}}
|
||||
</mat-icon>
|
||||
|
||||
<mat-error *ngIf="form.controls['newPassword'].hasError('required')">
|
||||
Please enter a new password
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="full-width">
|
||||
<input id="passwordConfirmInput" matInput placeholder="New Password Confirmation" formControlName="newPasswordConfirm" [type]="hideNewPasswordConfirm ? 'password' : 'text'" autocomplete="new-password">
|
||||
<mat-icon id="togglePasswordConfirmVisibility" matSuffix (click)="hideNewPasswordConfirm = !hideNewPasswordConfirm">
|
||||
{{hideNewPasswordConfirm ? 'visibility' : 'visibility_off'}}
|
||||
</mat-icon>
|
||||
|
||||
<mat-error *ngIf="form.controls['newPasswordConfirm'].hasError('required')">
|
||||
Please enter a your current password
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
</mat-card-content>
|
||||
|
||||
<mat-card-actions class="login-actions">
|
||||
<button id="submit" mat-raised-button color="primary" [disabled]="form.invalid || loading" (click)="resetPassword()">OK</button>
|
||||
<button id="cancel" mat-button (click)="cancel()">Back to Login</button>
|
||||
</mat-card-actions>
|
||||
|
||||
</mat-card>
|
||||
|
||||
<mat-progress-bar *ngIf="loading" mode="indeterminate"></mat-progress-bar>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, ParamMap } from '@angular/router';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { AuthenticationService } from 'src/app/core/services/auth.service';
|
||||
import { NotificationService } from 'src/app/core/services/notification.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-password-reset',
|
||||
templateUrl: './password-reset.component.html',
|
||||
styleUrls: ['./password-reset.component.css']
|
||||
})
|
||||
export class PasswordResetComponent implements OnInit {
|
||||
|
||||
private token!: string;
|
||||
email!: string;
|
||||
form!: UntypedFormGroup;
|
||||
loading!: boolean;
|
||||
hideNewPassword: boolean;
|
||||
hideNewPasswordConfirm: boolean;
|
||||
|
||||
constructor(private activeRoute: ActivatedRoute,
|
||||
private router: Router,
|
||||
private authService: AuthenticationService,
|
||||
private notificationService: NotificationService,
|
||||
private titleService: Title) {
|
||||
|
||||
this.titleService.setTitle('Jucundus - Password Reset');
|
||||
this.hideNewPassword = true;
|
||||
this.hideNewPasswordConfirm = true;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.activeRoute.queryParamMap.subscribe((params: ParamMap) => {
|
||||
this.token = params.get('token') + '';
|
||||
this.email = params.get('email') + '';
|
||||
|
||||
if (!this.token || !this.email) {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
});
|
||||
|
||||
this.form = new UntypedFormGroup({
|
||||
newPassword: new UntypedFormControl('', Validators.required),
|
||||
newPasswordConfirm: new UntypedFormControl('', Validators.required)
|
||||
});
|
||||
}
|
||||
|
||||
resetPassword() {
|
||||
|
||||
const password = this.form.get('newPassword')?.value;
|
||||
const passwordConfirm = this.form.get('newPasswordConfirm')?.value;
|
||||
|
||||
if (password !== passwordConfirm) {
|
||||
this.notificationService.openSnackBar('Passwords do not match');
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
this.authService.passwordReset(this.email, this.token, password, passwordConfirm)
|
||||
.subscribe(
|
||||
() => {
|
||||
this.notificationService.openSnackBar('Your password has been changed.');
|
||||
this.router.navigate(['/auth/login']);
|
||||
},
|
||||
(error: any) => {
|
||||
this.notificationService.openSnackBar(error.error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.router.navigate(['/']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th.mat-sort-header-sorted {
|
||||
color: black;
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<div class="container" fxLayout="row" fxLayoutAlign="center none">
|
||||
<div fxFlex="95%">
|
||||
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<h2>Customers</h2>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" matSort>
|
||||
|
||||
<!-- Position Column -->
|
||||
<ng-container matColumnDef="position">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> No. </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.position}} </td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> Name </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.name}} </td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Weight Column -->
|
||||
<ng-container matColumnDef="weight">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> Weight </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.weight}} </td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Symbol Column -->
|
||||
<ng-container matColumnDef="symbol">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header> Symbol </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.symbol}} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import { Component, OnInit, ViewChild } from '@angular/core';
|
||||
import { MatSort } from '@angular/material/sort';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { NGXLogger } from 'ngx-logger';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { NotificationService } from 'src/app/core/services/notification.service';
|
||||
|
||||
export interface PeriodicElement {
|
||||
name: string;
|
||||
position: number;
|
||||
weight: number;
|
||||
symbol: string;
|
||||
}
|
||||
|
||||
const ELEMENT_DATA: PeriodicElement[] = [
|
||||
{ position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H' },
|
||||
{ position: 2, name: 'Helium', weight: 4.0026, symbol: 'He' },
|
||||
{ position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li' },
|
||||
{ position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be' },
|
||||
{ position: 5, name: 'Boron', weight: 10.811, symbol: 'B' },
|
||||
{ position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C' },
|
||||
{ position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N' },
|
||||
{ position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O' },
|
||||
{ position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F' },
|
||||
{ position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne' },
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: 'app-customer-list',
|
||||
templateUrl: './customer-list.component.html',
|
||||
styleUrls: ['./customer-list.component.css']
|
||||
})
|
||||
export class CustomerListComponent implements OnInit {
|
||||
displayedColumns: string[] = ['position', 'name', 'weight', 'symbol'];
|
||||
dataSource = new MatTableDataSource(ELEMENT_DATA);
|
||||
|
||||
@ViewChild(MatSort, { static: true })
|
||||
sort: MatSort = new MatSort;
|
||||
|
||||
constructor(
|
||||
private logger: NGXLogger,
|
||||
private notificationService: NotificationService,
|
||||
private titleService: Title
|
||||
) { }
|
||||
|
||||
ngOnInit() {
|
||||
this.titleService.setTitle('Jucundus - Customers');
|
||||
this.logger.log('Customers loaded');
|
||||
this.notificationService.openSnackBar('Customers loaded');
|
||||
this.dataSource.sort = this.sort;
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { LayoutComponent } from 'src/app/shared/layout/layout.component';
|
||||
|
||||
import { CustomerListComponent } from './customer-list/customer-list.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LayoutComponent,
|
||||
children: [
|
||||
{ path: '', component: CustomerListComponent },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class CustomersRoutingModule { }
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CustomersRoutingModule } from './customers-routing.module';
|
||||
import { SharedModule } from 'src/app/shared/shared.module';
|
||||
import { CustomerListComponent } from './customer-list/customer-list.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
CustomersRoutingModule,
|
||||
SharedModule
|
||||
],
|
||||
declarations: [
|
||||
CustomerListComponent
|
||||
]
|
||||
})
|
||||
export class CustomersModule { }
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
.single-cards {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.single-card .mat-card-avatar {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.single-card .mat-icon {
|
||||
font-size: 55px;
|
||||
}
|
||||
|
||||
.projects-card>mat-card-content {
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<div class="container" fxLayout="row" fxLayoutAlign="center none">
|
||||
|
||||
<div fxFlex="95%">
|
||||
<div class="container" fxLayout="row" fxLayoutAlign="center none">
|
||||
|
||||
<h2>Welcome back, {{currentUser.fullName}}!</h2>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="container" fxLayout="row" fxLayoutAlign="center none">
|
||||
<div fxFlex="50%" class="text-center no-records animate">
|
||||
<mat-icon>dashboard</mat-icon>
|
||||
<p>This is the dashboard.</p>
|
||||
</div>
|
||||
<mat-icon> </mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { NotificationService } from 'src/app/core/services/notification.service';
|
||||
import { Title } from '@angular/platform-browser';
|
||||
import { NGXLogger } from 'ngx-logger';
|
||||
import { AuthenticationService } from 'src/app/core/services/auth.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-dashboard-home',
|
||||
templateUrl: './dashboard-home.component.html',
|
||||
styleUrls: ['./dashboard-home.component.css']
|
||||
})
|
||||
export class DashboardHomeComponent implements OnInit {
|
||||
currentUser: any;
|
||||
|
||||
constructor(private notificationService: NotificationService,
|
||||
private authService: AuthenticationService,
|
||||
private titleService: Title,
|
||||
private logger: NGXLogger) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.currentUser = this.authService.getCurrentUser();
|
||||
this.titleService.setTitle('Jucundus - Dashboard');
|
||||
this.logger.log('Dashboard loaded');
|
||||
|
||||
setTimeout(() => {
|
||||
this.notificationService.openSnackBar('Welcome!');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { Routes, RouterModule } from '@angular/router';
|
||||
import { LayoutComponent } from 'src/app/shared/layout/layout.component';
|
||||
|
||||
import { DashboardHomeComponent } from './dashboard-home/dashboard-home.component';
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: LayoutComponent,
|
||||
children: [
|
||||
{ path: '', component: DashboardHomeComponent },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(routes)],
|
||||
exports: [RouterModule]
|
||||
})
|
||||
export class DashboardRoutingModule { }
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { DashboardRoutingModule } from './dashboard-routing.module';
|
||||
import { DashboardHomeComponent } from './dashboard-home/dashboard-home.component';
|
||||
import { SharedModule } from 'src/app/shared/shared.module';
|
||||
|
||||
@NgModule({
|
||||
declarations: [DashboardHomeComponent],
|
||||
imports: [
|
||||
CommonModule,
|
||||
DashboardRoutingModule,
|
||||
SharedModule
|
||||
]
|
||||
})
|
||||
export class DashboardModule { }
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
mat-icon {
|
||||
color: rgb(200, 0, 0);
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<div class="container" fxLayout="row" fxLayoutAlign="center none">
|
||||
<div fxFlex="95%">
|
||||
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>New Lot</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div fxLayout="row" fxLayoutGap="5px">
|
||||
<mat-form-field>
|
||||
<mat-label>Url</mat-label>
|
||||
<input matInput placeholder="Ex. https://drouot.com/..." maxlength="255" st [(ngModel)]="url" >
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" (click)="openDialog()">Add</button>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<mat-card style="margin-top: 10px;">
|
||||
<mat-card-header>
|
||||
<mat-card-title>Favorites</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div fxLayout="row" fxLayoutGap="5px">
|
||||
<table mat-table [dataSource]="dataSource" 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" -->
|
||||
|
||||
<!-- Position 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="width: 50px"></td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Name Column -->
|
||||
<ng-container matColumnDef="lot">
|
||||
<th mat-header-cell *matHeaderCellDef> Lot </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.lotInfo.lotNumber}} </td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Weight Column -->
|
||||
<ng-container matColumnDef="title">
|
||||
<th mat-header-cell *matHeaderCellDef> Title </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.lotInfo.title}} </td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Symbol Column -->
|
||||
<ng-container matColumnDef="estimate">
|
||||
<th mat-header-cell *matHeaderCellDef> Estimate </th>
|
||||
<td mat-cell *matCellDef="let element"> {{element.lotInfo.EstimateLow}} - {{element.lotInfo.EstimateHigh}} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
import { apiService } from 'src/app/core/services/api.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-favorites-page',
|
||||
templateUrl: './favorites-page.component.html',
|
||||
styleUrls: ['./favorites-page.component.css']
|
||||
})
|
||||
export class FavoritesPageComponent implements OnInit {
|
||||
|
||||
url: string = '';
|
||||
displayedColumns: string[] = ['picture', 'lot', 'title', 'estimate'];
|
||||
dataSource = []
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private apiService: apiService) {}
|
||||
|
||||
openDialog(): void {
|
||||
|
||||
this.router.navigate(['favorites/new', this.url]);
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.apiService.getAllFavorite().subscribe((data: any) => {
|
||||
this.dataSource = data;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
.example-card {
|
||||
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>Picture</mat-card-title>
|
||||
<mat-card-subtitle>select the picture</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<mat-grid-list cols="2" >
|
||||
<mat-grid-tile *ngFor="let picture of images">
|
||||
<div style="width: 300px; height: 300px;">
|
||||
<img src="{{picture}}" (click)="onSelectImage(picture)" style="width: 100%; height: 100%; object-fit: contain;">
|
||||
</div>
|
||||
</mat-grid-tile>
|
||||
</mat-grid-list>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import { Component, OnInit, Inject } from '@angular/core';
|
||||
import {MatDialogRef, MAT_DIALOG_DATA} from '@angular/material/dialog';
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'change-image-dialog-dialog',
|
||||
templateUrl: './change-image-dialog.component.html',
|
||||
styleUrls: ['./change-image-dialog.component.css']
|
||||
})
|
||||
export class ChangeImageDialogComponent implements OnInit {
|
||||
|
||||
images: any[] = [];
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<ChangeImageDialogComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.images = this.data.images;
|
||||
console.log(this.images);
|
||||
}
|
||||
|
||||
onSelectImage(picture: any): void {
|
||||
this.dialogRef.close(picture);
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
mat-icon {
|
||||
color: rgb(200, 0, 0);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue