diff --git a/server/routers/dry-wash/arm-orders.js b/server/routers/dry-wash/arm-orders.js index ec9750b..ab10378 100644 --- a/server/routers/dry-wash/arm-orders.js +++ b/server/routers/dry-wash/arm-orders.js @@ -1,9 +1,13 @@ const router = require('express').Router() +const { OrderModel } = require('./model/order') -router.post('/orders', (req, res) => { - res - .status(200) - .send(require(`./json/arm-orders/success.json`)) +router.get('/orders', async (req, res, next) => { + try { + const orders = await OrderModel.find({}) + res.status(200).send({ success: true, body: orders }) + } catch (error) { + next(error) + } }) -module.exports = router +module.exports = router \ No newline at end of file diff --git a/server/routers/dry-wash/index.js b/server/routers/dry-wash/index.js index 202295b..5a89a80 100644 --- a/server/routers/dry-wash/index.js +++ b/server/routers/dry-wash/index.js @@ -1,10 +1,12 @@ const router = require('express').Router() const armMasterRouter = require('./arm-master') const armOrdersRouter = require('./arm-orders') +const orderRouter = require('./order') router.use('/arm', armMasterRouter) router.use('/arm', armOrdersRouter) +router.use('/order', orderRouter) module.exports = router diff --git a/server/routers/dry-wash/json/arm-orders/success.json b/server/routers/dry-wash/json/arm-orders/success.json index 15669e8..4bb99f9 100644 --- a/server/routers/dry-wash/json/arm-orders/success.json +++ b/server/routers/dry-wash/json/arm-orders/success.json @@ -2,24 +2,59 @@ "success": true, "body": [ { - "id": "order1", - "carNumber": "A123BC", - "startWashTime": "2024-11-24T10:30:00.000Z", - "endWashTime": "2024-11-24T16:30:00.000Z", - "orderDate": "2024-11-24T08:41:46.366Z", + "phone": "+79876543210", + "carNumber": "А123ВЕ16", + "carBody": 1, + "carColor": "#ffff00", + "startWashTime": "2025-05-12T08:21:00.000Z", + "endWashTime": "2025-05-12T08:22:00.000Z", + "location": "55.792799704829854,49.11034340707925 Республика Татарстан (Татарстан), Казань, улица Чернышевского", "status": "progress", - "phone": "79001234563", - "location": "Казань, ул. Баумана, 1" + "notes": "", + "created": "2025-01-18T17:43:21.488Z", + "updated": "2025-01-18T17:43:21.492Z" }, { - "id": "order2", - "carNumber": "A245BC", - "startWashTime": "2024-11-24T11:30:00.000Z", - "endWashTime": "2024-11-24T17:30:00.000Z", - "orderDate": "2024-11-24T07:40:46.366Z", + "phone": "89876543210", + "carNumber": "К456МН23", + "carBody": 2, + "carColor": "#ffffff", + "startWashTime": "2025-01-12T08:21:00Z", + "endWashTime": "2025-01-12T08:22:00Z", + "location": "55.808430668108585,49.198608125449255 Республика Татарстан (Татарстан), Казань, улица Академика Губкина, 50/1", + "status": "pending", + "notes": "заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки заметки", + "created": "2025-01-18T17:46:10.388Z", + "updated": "2025-01-18T17:46:10.395Z", + "id": "678be8e211e62f4a61790cca" + }, + { + "phone": "4098765432105", + "carNumber": "О789РС777", + "carBody": 3, + "carColor": "красный", + "startWashTime": "2025-08-12T08:21:00.000Z", + "endWashTime": "2025-08-12T08:22:00.000Z", + "location": "55.78720449830353,49.12111640202319 Республика Татарстан (Татарстан), Казань, улица Пушкина, 5/43", + "status": "cancelled", + "notes": "Заказ отменен по запросу самого клиента", + "created": "2025-01-18T17:47:46.294Z", + "updated": "2025-01-18T17:47:46.295Z", + "id": "678be8e211e62f4a61790ccb" + }, + { + "phone": "+79876543210", + "carNumber": "Т123УХ716", + "carBody": 99, + "carColor": "чайная роза", + "startWashTime": "2025-01-11T11:21:00.000Z", + "endWashTime": "2025-01-12T11:22:00.000Z", + "location": "55.77063673480112,49.22182909159608 Республика Татарстан (Татарстан), Казань, Советский район, микрорайон Азино-2", "status": "progress", - "phone": "79001234567", - "location": "Казань, ул. Баумана, 43" + "notes": "Клиент остался доволен, предложить в следующий раз акцию", + "created": "2025-01-18T17:55:05.691Z", + "updated": "2025-01-18T17:55:05.695Z", + "id": "678be8e211e62f4a61790ccc" } ] } \ No newline at end of file diff --git a/server/routers/dry-wash/model/const.js b/server/routers/dry-wash/model/const.js new file mode 100644 index 0000000..6805955 --- /dev/null +++ b/server/routers/dry-wash/model/const.js @@ -0,0 +1,11 @@ +const orderStatus = { + CANCELLED: 'cancelled', + PROGRESS: 'progress', + PENDING: 'pending', + WORKING: 'working', + COMPLETE: 'complete', +} + +module.exports = { + orderStatus +} \ No newline at end of file diff --git a/server/routers/dry-wash/model/order.js b/server/routers/dry-wash/model/order.js index deb0c10..6fdaa25 100644 --- a/server/routers/dry-wash/model/order.js +++ b/server/routers/dry-wash/model/order.js @@ -1,26 +1,58 @@ const { Schema, model } = require('mongoose') +const { orderStatus } = require('./const') const schema = new Schema({ - startWashTime: {type: String, required: true}, - endWashTime: {type: String, required: true}, - orderDate: {type: String, required: true}, - location: {type: String, required: true}, - phone: {type: String, required: true}, - status: {type: String, required: true}, - carNumber: {type: String, required: true}, + phone: { + type: String, + required: true + }, + carNumber: { + type: String, + required: true + }, + carBody: { + type: Number, + required: true + }, + carColor: String, + startWashTime: { + type: Date, + required: true + }, + endWashTime: { + type: Date, + required: true + }, + location: { + type: String, + required: true + }, + status: { + type: String, + required: true, + enum: Object.values(orderStatus) + }, + master: { + type: Schema.Types.ObjectId, + ref: 'dry-wash-master' + }, + notes: String, created: { - type: Date, default: () => new Date().toISOString(), + type: Date, + default: () => new Date().toISOString(), }, updated: { - type: Date, default: () => new Date().toISOString(), + type: Date, + default: () => new Date().toISOString(), }, - master: {type: Schema.Types.ObjectId, ref: 'dry-wash-master'}, - notes: String, }) schema.set('toJSON', { virtuals: true, versionKey: false, + transform(_doc, ret) { + delete ret._id + } }) schema.virtual('id').get(function () { diff --git a/server/routers/dry-wash/order.js b/server/routers/dry-wash/order.js new file mode 100644 index 0000000..d258fa0 --- /dev/null +++ b/server/routers/dry-wash/order.js @@ -0,0 +1,251 @@ +const mongoose = require("mongoose") +const router = require('express').Router() +const { MasterModel } = require('./model/master') +const { OrderModel } = require('./model/order') +const { orderStatus } = require('./model/const') + +const isValidPhoneNumber = (value) => /^(\+)?\d{9,15}/.test(value) +const isValidCarNumber = (value) => /^[авекмнорстух][0-9]{3}[авекмнорстух]{2}[0-9]{2,3}$/i.test(value) +const isValidCarBodyType = (value) => typeof value === 'number' && value > 0 && value < 100 +const isValidCarColor = (value) => value.length < 50 && /^[#a-z0-9а-я-\s,.()]+$/i.test(value) +const isValidISODate = (value) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.\d{1,3})?Z$/.test(value) + +const latitudeRe = /^(-?[1-8]?\d(?:\.\d{1,18})?|90(?:\.0{1,18})?)$/ +const longitudeRe = /^(-?(?:1[0-7]|[1-9])?\d(?:\.\d{1,18})?|180(?:\.0{1,18})?)$/ +const addressRe = /^[а-я0-9\s,.'-/()]*$/i +const isValidLocation = (value) => { + if (value.length > 200) { + return false + } + + const [coordinates, address] = value.split(' ') + const [latitude, longitude] = coordinates.split(',') + return latitudeRe.test(latitude) && longitudeRe.test(longitude) && addressRe.test(address) +} + +const isValidOrderStatus = (value) => Object.values(orderStatus).includes(value) +const isValidOrderNotes = (value) => value.length < 500 + +const VALIDATION_MESSAGES = { + order: { + notFound: 'Order not found' + }, + orderId: { + invalid: 'Valid order ID is required', + }, + orderStatus: { + invalid: 'Invalid order status' + }, + orderNotes: { + invalid: 'Invalid order notes' + }, + master: { + notFound: 'Master not found' + }, + masterId: { + invalid: 'Invalid master ID', + }, + phoneNumber: { + required: 'Phone number is required', + invalid: 'Invalid phone number' + }, + carNumber: { + required: 'Car number is required', + invalid: 'Invalid car number' + }, + carBody: { + required: 'Car body type is required', + invalid: 'Invalid car body type' + }, + carColor: { + invalid: 'Invalid car color' + }, + washingBegin: { + required: 'Begin time of washing is required', + invalid: 'Invalid begin time of washing' + }, + washingEnd: { + required: 'End time of washing is required', + invalid: 'Invalid end time of washing' + }, + washingLocation: { + required: 'Location of washing is required', + invalid: 'Invalid location of washing' + }, +} + +router.post('/create', async (req, res, next) => { + const bodyErrors = [] + + const { customer } = req.body + if (!customer.phone) { + bodyErrors.push(VALIDATION_MESSAGES.phoneNumber.required) + } else if (!isValidPhoneNumber(customer.phone)) { + bodyErrors.push(VALIDATION_MESSAGES.phoneNumber.invalid) + } + + const { car } = req.body + if (!car.number) { + bodyErrors.push(VALIDATION_MESSAGES.carNumber.required) + } else if (!isValidCarNumber(car.number)) { + bodyErrors.push(VALIDATION_MESSAGES.carNumber.invalid) + } + if (!car.body) { + bodyErrors.push(VALIDATION_MESSAGES.carBody.required) + } else if (!isValidCarBodyType(car.body)) { + bodyErrors.push(VALIDATION_MESSAGES.carBody.invalid) + } + if (!isValidCarColor(car.color)) { + bodyErrors.push(VALIDATION_MESSAGES.carColor.invalid) + } + + const { washing } = req.body + if (!washing.begin) { + bodyErrors.push(VALIDATION_MESSAGES.washingBegin.required) + } else if (!isValidISODate(washing.begin)) { + bodyErrors.push(VALIDATION_MESSAGES.washingBegin.invalid) + } + if (!washing.end) { + bodyErrors.push(VALIDATION_MESSAGES.washingEnd.required) + } else if (!isValidISODate(washing.end)) { + bodyErrors.push(VALIDATION_MESSAGES.washingEnd.invalid) + } + if (!washing.location) { + bodyErrors.push(VALIDATION_MESSAGES.washingLocation.required) + } else if (!isValidLocation(washing.location)) { + bodyErrors.push(VALIDATION_MESSAGES.washingLocation.invalid) + } + + if (bodyErrors.length > 0) { + throw new Error(bodyErrors.join(', ')) + } + + try { + const order = await OrderModel.create({ + phone: customer.phone, + carNumber: car.number, + carBody: car.body, + carColor: car.color, + startWashTime: washing.begin, + endWashTime: washing.end, + location: washing.location, + status: orderStatus.PROGRESS, + notes: '', + created: new Date().toISOString(), + }) + + res.status(200).send({ success: true, body: order }) + + } catch (error) { + next(error) + } +}) + +router.get('/:id', async (req, res, next) => { + const { id } = req.params + if (!mongoose.Types.ObjectId.isValid(id)) { + throw new Error(VALIDATION_MESSAGES.orderId.invalid) + } + + try { + const order = await OrderModel.findById(id) + if (!order) { + throw new Error(VALIDATION_MESSAGES.order.notFound) + } + + res.status(200).send({ success: true, body: order }) + } catch (error) { + next(error) + } +}) + +router.patch('/:id', async (req, res, next) => { + const { id } = req.params + if (!mongoose.Types.ObjectId.isValid(id)) { + throw new Error(VALIDATION_MESSAGES.orderId.invalid) + } + + const bodyErrors = [] + + const { status } = req.body + if (status) { + if (!isValidOrderStatus(status)) { + bodyErrors.push(VALIDATION_MESSAGES.orderStatus.invalid) + } + } + + const { master: masterId } = req.body + if (masterId) { + if (!mongoose.Types.ObjectId.isValid(masterId)) { + bodyErrors.push(VALIDATION_MESSAGES.masterId.invalid) + } else { + try { + const master = await MasterModel.findById(masterId) + if (!master) { + bodyErrors.push(VALIDATION_MESSAGES.master.notFound) + } + } catch (error) { + next(error) + } + } + } + + const { notes } = req.body + if (notes) { + if (!isValidOrderNotes(notes)) { + bodyErrors.push(VALIDATION_MESSAGES.orderNotes.invalid) + } + } + + if (bodyErrors.length > 0) { + throw new Error(bodyErrors.join(', ')) + } + + try { + const updateData = {} + if (status) { + updateData.status = status + } + if (masterId) { + updateData.master = masterId + } + if (notes) { + updateData.notes = notes + } + updateData.updated = new Date().toISOString() + + const order = await OrderModel.findByIdAndUpdate( + id, + updateData, + { new: true } + ) + if (!order) { + throw new Error(VALIDATION_MESSAGES.order.notFound) + } + + res.status(200).send({ success: true, body: order }) + } catch (error) { + next(error) + } +}) + +router.delete('/:id', async (req, res, next) => { + const { id } = req.params + if (!mongoose.Types.ObjectId.isValid(id)) { + throw new Error(VALIDATION_MESSAGES.orderId.invalid) + } + + try { + const order = await OrderModel.findByIdAndDelete(id, { + new: true, + }) + if (!order) { + throw new Error(VALIDATION_MESSAGES.order.notFound) + } + res.status(200).send({ success: true, body: order }) + } catch (error) { + next(error) + } +}) + +module.exports = router \ No newline at end of file