feat: complete day 2
This commit is contained in:
+11
@@ -1,2 +1,13 @@
|
|||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Environment variables in day projects
|
||||||
|
day*/.env
|
||||||
|
day*/.env.*
|
||||||
|
day*/**/.env
|
||||||
|
|
||||||
|
# Node modules in day projects
|
||||||
|
day*/node_modules/
|
||||||
|
|
||||||
|
day*/package-lock.json
|
||||||
@@ -38,7 +38,7 @@ router.post("/", async (req, res) => {
|
|||||||
return handleError("Name is required and cannot be empty", 400, res);
|
return handleError("Name is required and cannot be empty", 400, res);
|
||||||
}
|
}
|
||||||
await ShippingDock.create(req.body);
|
await ShippingDock.create(req.body);
|
||||||
handleSuccess(res, dock, 201);
|
handleSuccess(res, null, 201);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleError(err.message, 500, res);
|
handleError(err.message, 500, res);
|
||||||
}
|
}
|
||||||
@@ -85,7 +85,7 @@ router.delete("/:id", async (req, res) => {
|
|||||||
where: { id: req.params.id },
|
where: { id: req.params.id },
|
||||||
});
|
});
|
||||||
if (deleted === 0) return handleError("Dock not found", 404, res);
|
if (deleted === 0) return handleError("Dock not found", 404, res);
|
||||||
handleSuccess(res, null, 204); // 204 No Content
|
handleSuccess(res, null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
handleSequelizeError(err, res);
|
handleSequelizeError(err, res);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
module.exports = (sequelize, DataTypes) => {
|
||||||
|
const order = sequelize.define(
|
||||||
|
"order",
|
||||||
|
{
|
||||||
|
id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
primaryKey: true,
|
||||||
|
autoIncrement: true,
|
||||||
|
},
|
||||||
|
user_id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
shipping_dock_id: {
|
||||||
|
type: DataTypes.INTEGER,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
amount: {
|
||||||
|
type: DataTypes.DECIMAL(10, 2),
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: DataTypes.STRING,
|
||||||
|
defaultValue: "pending",
|
||||||
|
},
|
||||||
|
created_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
updated_at: {
|
||||||
|
type: DataTypes.DATE,
|
||||||
|
defaultValue: DataTypes.NOW,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
freezeTableName: true,
|
||||||
|
tableName: "order",
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return order;
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const db = require("../models");
|
||||||
|
const PaginationService = require("../services/PaginationService");
|
||||||
|
const ReportService = require("../services/ReportService");
|
||||||
|
|
||||||
|
// GET /api/v1/order/odd - Return all odd order_id rows
|
||||||
|
router.get("/odd", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const oddOrders = await PaginationService.getOddOrders(db.order);
|
||||||
|
res.json(oddOrders);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/v1/order - Paginate orders with optional sorting
|
||||||
|
router.get("/", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { page = 1, limit = 10, sort = "id", direction = "ASC" } = req.query;
|
||||||
|
|
||||||
|
const result = await PaginationService.paginateWithSort(db.order, {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
sort,
|
||||||
|
direction,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/v1/order/cursor - Cursor-based pagination
|
||||||
|
router.get("/cursor", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id, limit = 10 } = req.query;
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return res.status(400).json({ error: "id parameter is required" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await PaginationService.paginateWithCursor(db.order, {
|
||||||
|
id,
|
||||||
|
limit,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const router = express.Router();
|
||||||
|
const db = require("../models");
|
||||||
|
const ReportService = require("../services/ReportService");
|
||||||
|
|
||||||
|
// GET /api/v1/report/sale?month=1&year=2022
|
||||||
|
router.get("/sale", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { month, year, from_date, to_date } = req.query;
|
||||||
|
if (month && year) {
|
||||||
|
const result = await ReportService.getMonthlySales(db.order, month, year);
|
||||||
|
return res.json(result);
|
||||||
|
} else if (from_date && to_date) {
|
||||||
|
const result = await ReportService.getSalesByDateRange(
|
||||||
|
db.order,
|
||||||
|
from_date,
|
||||||
|
to_date
|
||||||
|
);
|
||||||
|
return res.json(result);
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ error: "Missing required parameters" });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/v1/report/monthly?year=2022
|
||||||
|
router.get("/monthly", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { year } = req.query;
|
||||||
|
if (!year) return res.status(400).json({ error: "year is required" });
|
||||||
|
const result = await ReportService.getMonthlySalesByYear(db.order, year);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/v1/report/user?year=2022&user_id=1
|
||||||
|
router.get("/user", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { year, user_id } = req.query;
|
||||||
|
if (!year || !user_id)
|
||||||
|
return res.status(400).json({ error: "year and user_id are required" });
|
||||||
|
const result = await ReportService.getMonthlySalesByUser(
|
||||||
|
db.order,
|
||||||
|
year,
|
||||||
|
user_id
|
||||||
|
);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/v1/report/shipping_dock?year=2022&shipping_dock_id=1
|
||||||
|
router.get("/shipping_dock", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { year, shipping_dock_id } = req.query;
|
||||||
|
if (!year || !shipping_dock_id)
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ error: "year and shipping_dock_id are required" });
|
||||||
|
const result = await ReportService.getMonthlySalesByShippingDock(
|
||||||
|
db.order,
|
||||||
|
year,
|
||||||
|
shipping_dock_id
|
||||||
|
);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/v1/report/user/count?year=2022&user_id=1
|
||||||
|
router.get("/user/count", async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { year, user_id } = req.query;
|
||||||
|
if (!year || !user_id)
|
||||||
|
return res.status(400).json({ error: "year and user_id are required" });
|
||||||
|
const result = await ReportService.getOrderCountByUser(
|
||||||
|
db.order,
|
||||||
|
year,
|
||||||
|
user_id
|
||||||
|
);
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
const { Op } = require("sequelize");
|
||||||
|
|
||||||
|
class PaginationService {
|
||||||
|
/**
|
||||||
|
* Handle offset-based pagination with sorting
|
||||||
|
*/
|
||||||
|
static async paginateWithSort(model, options = {}) {
|
||||||
|
const {
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
sort = "id",
|
||||||
|
direction = "ASC",
|
||||||
|
where = {},
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const offset = (page - 1) * limit;
|
||||||
|
const order = [[sort, direction.toUpperCase()]];
|
||||||
|
|
||||||
|
const { count, rows } = await model.findAndCountAll({
|
||||||
|
where,
|
||||||
|
order,
|
||||||
|
limit: parseInt(limit),
|
||||||
|
offset: parseInt(offset),
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = Math.ceil(count / limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
total: count,
|
||||||
|
page: parseInt(page),
|
||||||
|
totalPages,
|
||||||
|
list: rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle cursor-based pagination
|
||||||
|
*/
|
||||||
|
static async paginateWithCursor(model, options = {}) {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
limit = 10,
|
||||||
|
sort = "id",
|
||||||
|
direction = "ASC",
|
||||||
|
where = {},
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const order = [[sort, direction.toUpperCase()]];
|
||||||
|
const cursorWhere = {
|
||||||
|
...where,
|
||||||
|
[sort]: {
|
||||||
|
[direction.toUpperCase() === "ASC" ? Op.gt : Op.lt]: id,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const rows = await model.findAll({
|
||||||
|
where: cursorWhere,
|
||||||
|
order,
|
||||||
|
limit: parseInt(limit),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: parseInt(id),
|
||||||
|
list: rows,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get odd order IDs
|
||||||
|
*/
|
||||||
|
static async getOddOrders(model) {
|
||||||
|
return await model.findAll({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
[Op.mod]: [2, 1], // id % 2 = 1 (odd numbers)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = PaginationService;
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
const { Op, fn, col, literal } = require("sequelize");
|
||||||
|
|
||||||
|
class ReportService {
|
||||||
|
/**
|
||||||
|
* Get total sales for a specific month and year
|
||||||
|
*/
|
||||||
|
static async getMonthlySales(model, month, year) {
|
||||||
|
const result = await model.findOne({
|
||||||
|
attributes: [[fn("SUM", col("amount")), "total_amount"]],
|
||||||
|
where: {
|
||||||
|
created_at: {
|
||||||
|
[Op.and]: [
|
||||||
|
literal(`MONTH(created_at) = ${month}`),
|
||||||
|
literal(`YEAR(created_at) = ${year}`),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
month: parseInt(month),
|
||||||
|
year: parseInt(year),
|
||||||
|
total_amount: parseFloat(result?.dataValues?.total_amount || 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get total sales for a date range
|
||||||
|
*/
|
||||||
|
static async getSalesByDateRange(model, fromDate, toDate) {
|
||||||
|
// Ensure dates are in correct order
|
||||||
|
const startDate = new Date(fromDate) < new Date(toDate) ? fromDate : toDate;
|
||||||
|
const endDate = new Date(fromDate) < new Date(toDate) ? toDate : fromDate;
|
||||||
|
|
||||||
|
const result = await model.findOne({
|
||||||
|
attributes: [[fn("SUM", col("amount")), "total_amount"]],
|
||||||
|
where: {
|
||||||
|
created_at: {
|
||||||
|
[Op.between]: [startDate, endDate],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
from_date: startDate,
|
||||||
|
to_date: endDate,
|
||||||
|
total_amount: parseFloat(result?.dataValues?.total_amount || 0),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get monthly sales for a year (only months with sales > 0)
|
||||||
|
*/
|
||||||
|
static async getMonthlySalesByYear(model, year) {
|
||||||
|
const results = await model.findAll({
|
||||||
|
attributes: [
|
||||||
|
[fn("MONTH", col("created_at")), "month"],
|
||||||
|
[fn("SUM", col("amount")), "total_amount"],
|
||||||
|
],
|
||||||
|
where: {
|
||||||
|
created_at: {
|
||||||
|
[Op.and]: [literal(`YEAR(created_at) = ${year}`)],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
group: [fn("MONTH", col("created_at"))],
|
||||||
|
having: literal("SUM(amount) > 0"),
|
||||||
|
order: [[fn("MONTH", col("created_at")), "ASC"]],
|
||||||
|
});
|
||||||
|
|
||||||
|
return results.map((result) => ({
|
||||||
|
month: parseInt(result.dataValues.month),
|
||||||
|
total_amount: parseFloat(result.dataValues.total_amount),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get monthly sales for a year by user_id (only months with sales > 0)
|
||||||
|
*/
|
||||||
|
static async getMonthlySalesByUser(model, year, userId) {
|
||||||
|
const results = await model.findAll({
|
||||||
|
attributes: [
|
||||||
|
[fn("MONTH", col("created_at")), "month"],
|
||||||
|
[fn("SUM", col("amount")), "total_amount"],
|
||||||
|
],
|
||||||
|
where: {
|
||||||
|
created_at: {
|
||||||
|
[Op.and]: [literal(`YEAR(created_at) = ${year}`)],
|
||||||
|
},
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
group: [fn("MONTH", col("created_at"))],
|
||||||
|
having: literal("SUM(amount) > 0"),
|
||||||
|
order: [[fn("MONTH", col("created_at")), "ASC"]],
|
||||||
|
});
|
||||||
|
|
||||||
|
return results.map((result) => ({
|
||||||
|
month: parseInt(result.dataValues.month),
|
||||||
|
total_amount: parseFloat(result.dataValues.total_amount),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get monthly sales for a year by shipping_dock_id (only months with sales > 0)
|
||||||
|
*/
|
||||||
|
static async getMonthlySalesByShippingDock(model, year, shippingDockId) {
|
||||||
|
const results = await model.findAll({
|
||||||
|
attributes: [
|
||||||
|
[fn("MONTH", col("created_at")), "month"],
|
||||||
|
[fn("SUM", col("amount")), "total_amount"],
|
||||||
|
],
|
||||||
|
where: {
|
||||||
|
created_at: {
|
||||||
|
[Op.and]: [literal(`YEAR(created_at) = ${year}`)],
|
||||||
|
},
|
||||||
|
shipping_dock_id: shippingDockId,
|
||||||
|
},
|
||||||
|
group: [fn("MONTH", col("created_at"))],
|
||||||
|
having: literal("SUM(amount) > 0"),
|
||||||
|
order: [[fn("MONTH", col("created_at")), "ASC"]],
|
||||||
|
});
|
||||||
|
|
||||||
|
return results.map((result) => ({
|
||||||
|
month: parseInt(result.dataValues.month),
|
||||||
|
total_amount: parseFloat(result.dataValues.total_amount),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get order count per month for a user in a year (include months with 0 orders)
|
||||||
|
*/
|
||||||
|
static async getOrderCountByUser(model, year, userId) {
|
||||||
|
// First get all months with orders
|
||||||
|
const orderMonths = await model.findAll({
|
||||||
|
attributes: [
|
||||||
|
[fn("MONTH", col("created_at")), "month"],
|
||||||
|
[fn("COUNT", col("id")), "order_count"],
|
||||||
|
],
|
||||||
|
where: {
|
||||||
|
created_at: {
|
||||||
|
[Op.and]: [literal(`YEAR(created_at) = ${year}`)],
|
||||||
|
},
|
||||||
|
user_id: userId,
|
||||||
|
},
|
||||||
|
group: [fn("MONTH", col("created_at"))],
|
||||||
|
order: [[fn("MONTH", col("created_at")), "ASC"]],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a map of months with orders
|
||||||
|
const monthMap = {};
|
||||||
|
orderMonths.forEach((result) => {
|
||||||
|
monthMap[parseInt(result.dataValues.month)] = parseInt(
|
||||||
|
result.dataValues.order_count
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create result array with all 12 months
|
||||||
|
const result = [];
|
||||||
|
for (let month = 1; month <= 12; month++) {
|
||||||
|
result.push({
|
||||||
|
month,
|
||||||
|
order_count: monthMap[month] || 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ReportService;
|
||||||
Reference in New Issue
Block a user