Compare commits
No commits in common. "262718dc6617f3b50d9e3f6ba020c4447cd788c5" and "b706f83f952ade63c8776e5266bd9a21c1338592" have entirely different histories.
262718dc66
...
b706f83f95
5
.gitignore
vendored
5
.gitignore
vendored
@ -1,8 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
*/node_modules/
|
*/node_modules/
|
||||||
*/build/
|
*/build/
|
||||||
|
|
||||||
**/node_modules/
|
|
||||||
**/build/
|
|
||||||
.DS_Store
|
|
||||||
.env
|
|
||||||
File diff suppressed because one or more lines are too long
@ -19,9 +19,6 @@ module.exports = class BookingsDBApi {
|
|||||||
start_date: data.start_date || null,
|
start_date: data.start_date || null,
|
||||||
end_date: data.end_date || null,
|
end_date: data.end_date || null,
|
||||||
status: data.status || null,
|
status: data.status || null,
|
||||||
vehiclenumber: data.vehiclenumber || null,
|
|
||||||
phone: data.phone || null,
|
|
||||||
email: data.email || null,
|
|
||||||
importHash: data.importHash || null,
|
importHash: data.importHash || null,
|
||||||
createdById: currentUser.id,
|
createdById: currentUser.id,
|
||||||
updatedById: currentUser.id,
|
updatedById: currentUser.id,
|
||||||
@ -48,9 +45,6 @@ module.exports = class BookingsDBApi {
|
|||||||
start_date: item.start_date || null,
|
start_date: item.start_date || null,
|
||||||
end_date: item.end_date || null,
|
end_date: item.end_date || null,
|
||||||
status: item.status || null,
|
status: item.status || null,
|
||||||
vehiclenumber: item.vehiclenumber || null,
|
|
||||||
phone: item.phone || null,
|
|
||||||
email: item.email || null,
|
|
||||||
importHash: item.importHash || null,
|
importHash: item.importHash || null,
|
||||||
createdById: currentUser.id,
|
createdById: currentUser.id,
|
||||||
updatedById: currentUser.id,
|
updatedById: currentUser.id,
|
||||||
@ -84,13 +78,6 @@ module.exports = class BookingsDBApi {
|
|||||||
|
|
||||||
if (data.status !== undefined) updatePayload.status = data.status;
|
if (data.status !== undefined) updatePayload.status = data.status;
|
||||||
|
|
||||||
if (data.vehiclenumber !== undefined)
|
|
||||||
updatePayload.vehiclenumber = data.vehiclenumber;
|
|
||||||
|
|
||||||
if (data.phone !== undefined) updatePayload.phone = data.phone;
|
|
||||||
|
|
||||||
if (data.email !== undefined) updatePayload.email = data.email;
|
|
||||||
|
|
||||||
updatePayload.updatedById = currentUser.id;
|
updatePayload.updatedById = currentUser.id;
|
||||||
|
|
||||||
await bookings.update(updatePayload, { transaction });
|
await bookings.update(updatePayload, { transaction });
|
||||||
@ -226,31 +213,6 @@ module.exports = class BookingsDBApi {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter.vehiclenumber) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
[Op.and]: Utils.ilike(
|
|
||||||
'bookings',
|
|
||||||
'vehiclenumber',
|
|
||||||
filter.vehiclenumber,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.phone) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
[Op.and]: Utils.ilike('bookings', 'phone', filter.phone),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.email) {
|
|
||||||
where = {
|
|
||||||
...where,
|
|
||||||
[Op.and]: Utils.ilike('bookings', 'email', filter.email),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filter.calendarStart && filter.calendarEnd) {
|
if (filter.calendarStart && filter.calendarEnd) {
|
||||||
where = {
|
where = {
|
||||||
...where,
|
...where,
|
||||||
|
|||||||
@ -1,49 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
/**
|
|
||||||
* @param {QueryInterface} queryInterface
|
|
||||||
* @param {Sequelize} Sequelize
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
/**
|
|
||||||
* @type {Transaction}
|
|
||||||
*/
|
|
||||||
const transaction = await queryInterface.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
'bookings',
|
|
||||||
'vehiclenumber',
|
|
||||||
{
|
|
||||||
type: Sequelize.DataTypes.TEXT,
|
|
||||||
},
|
|
||||||
{ transaction },
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (err) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* @param {QueryInterface} queryInterface
|
|
||||||
* @param {Sequelize} Sequelize
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async down(queryInterface, Sequelize) {
|
|
||||||
/**
|
|
||||||
* @type {Transaction}
|
|
||||||
*/
|
|
||||||
const transaction = await queryInterface.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await queryInterface.removeColumn('bookings', 'vehiclenumber', {
|
|
||||||
transaction,
|
|
||||||
});
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (err) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
/**
|
|
||||||
* @param {QueryInterface} queryInterface
|
|
||||||
* @param {Sequelize} Sequelize
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
/**
|
|
||||||
* @type {Transaction}
|
|
||||||
*/
|
|
||||||
const transaction = await queryInterface.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
'bookings',
|
|
||||||
'phone',
|
|
||||||
{
|
|
||||||
type: Sequelize.DataTypes.TEXT,
|
|
||||||
},
|
|
||||||
{ transaction },
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (err) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* @param {QueryInterface} queryInterface
|
|
||||||
* @param {Sequelize} Sequelize
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async down(queryInterface, Sequelize) {
|
|
||||||
/**
|
|
||||||
* @type {Transaction}
|
|
||||||
*/
|
|
||||||
const transaction = await queryInterface.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await queryInterface.removeColumn('bookings', 'phone', { transaction });
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (err) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
/**
|
|
||||||
* @param {QueryInterface} queryInterface
|
|
||||||
* @param {Sequelize} Sequelize
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async up(queryInterface, Sequelize) {
|
|
||||||
/**
|
|
||||||
* @type {Transaction}
|
|
||||||
*/
|
|
||||||
const transaction = await queryInterface.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await queryInterface.addColumn(
|
|
||||||
'bookings',
|
|
||||||
'email',
|
|
||||||
{
|
|
||||||
type: Sequelize.DataTypes.TEXT,
|
|
||||||
},
|
|
||||||
{ transaction },
|
|
||||||
);
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (err) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* @param {QueryInterface} queryInterface
|
|
||||||
* @param {Sequelize} Sequelize
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
async down(queryInterface, Sequelize) {
|
|
||||||
/**
|
|
||||||
* @type {Transaction}
|
|
||||||
*/
|
|
||||||
const transaction = await queryInterface.sequelize.transaction();
|
|
||||||
try {
|
|
||||||
await queryInterface.removeColumn('bookings', 'email', { transaction });
|
|
||||||
|
|
||||||
await transaction.commit();
|
|
||||||
} catch (err) {
|
|
||||||
await transaction.rollback();
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@ -32,18 +32,6 @@ module.exports = function (sequelize, DataTypes) {
|
|||||||
values: ['Pending', 'Inspected', 'Cancelled'],
|
values: ['Pending', 'Inspected', 'Cancelled'],
|
||||||
},
|
},
|
||||||
|
|
||||||
vehiclenumber: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
},
|
|
||||||
|
|
||||||
phone: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
},
|
|
||||||
|
|
||||||
email: {
|
|
||||||
type: DataTypes.TEXT,
|
|
||||||
},
|
|
||||||
|
|
||||||
importHash: {
|
importHash: {
|
||||||
type: DataTypes.STRING(255),
|
type: DataTypes.STRING(255),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|||||||
@ -22,12 +22,6 @@ const BookingsData = [
|
|||||||
// type code here for "relation_one" field
|
// type code here for "relation_one" field
|
||||||
|
|
||||||
status: 'Inspected',
|
status: 'Inspected',
|
||||||
|
|
||||||
vehiclenumber: 'Emil Kraepelin',
|
|
||||||
|
|
||||||
phone: 'Lynn Margulis',
|
|
||||||
|
|
||||||
email: 'Sigmund Freud',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -39,13 +33,7 @@ const BookingsData = [
|
|||||||
|
|
||||||
// type code here for "relation_one" field
|
// type code here for "relation_one" field
|
||||||
|
|
||||||
status: 'Pending',
|
status: 'Inspected',
|
||||||
|
|
||||||
vehiclenumber: 'Max Planck',
|
|
||||||
|
|
||||||
phone: 'Tycho Brahe',
|
|
||||||
|
|
||||||
email: 'Francis Galton',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -57,13 +45,7 @@ const BookingsData = [
|
|||||||
|
|
||||||
// type code here for "relation_one" field
|
// type code here for "relation_one" field
|
||||||
|
|
||||||
status: 'Cancelled',
|
status: 'Inspected',
|
||||||
|
|
||||||
vehiclenumber: 'Emil Fischer',
|
|
||||||
|
|
||||||
phone: 'Francis Crick',
|
|
||||||
|
|
||||||
email: 'Jean Baptiste Lamarck',
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -75,13 +57,19 @@ const BookingsData = [
|
|||||||
|
|
||||||
// type code here for "relation_one" field
|
// type code here for "relation_one" field
|
||||||
|
|
||||||
|
status: 'Cancelled',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
title: 'Vehicle Inspection - William',
|
||||||
|
|
||||||
|
start_date: new Date('2023-11-05T11:00:00Z'),
|
||||||
|
|
||||||
|
end_date: new Date('2023-11-05T12:00:00Z'),
|
||||||
|
|
||||||
|
// type code here for "relation_one" field
|
||||||
|
|
||||||
status: 'Inspected',
|
status: 'Inspected',
|
||||||
|
|
||||||
vehiclenumber: 'Nicolaus Copernicus',
|
|
||||||
|
|
||||||
phone: 'Charles Sherrington',
|
|
||||||
|
|
||||||
email: 'Ernest Rutherford',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -101,6 +89,10 @@ const CategoriesData = [
|
|||||||
{
|
{
|
||||||
name: 'Startup',
|
name: 'Startup',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'Non-Profit',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const ContactsData = [
|
const ContactsData = [
|
||||||
@ -143,6 +135,16 @@ const ContactsData = [
|
|||||||
|
|
||||||
// type code here for "relation_one" field
|
// type code here for "relation_one" field
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
first_name: 'Eve',
|
||||||
|
|
||||||
|
last_name: 'Adams',
|
||||||
|
|
||||||
|
email: 'eve.adams@example.com',
|
||||||
|
|
||||||
|
// type code here for "relation_one" field
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const DepartmentsData = [
|
const DepartmentsData = [
|
||||||
@ -169,13 +171,19 @@ const DepartmentsData = [
|
|||||||
|
|
||||||
// type code here for "relation_many" field
|
// type code here for "relation_many" field
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'IT',
|
||||||
|
|
||||||
|
// type code here for "relation_many" field
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const LeadsData = [
|
const LeadsData = [
|
||||||
{
|
{
|
||||||
name: 'Acme Corp',
|
name: 'Acme Corp',
|
||||||
|
|
||||||
status: 'Contacted',
|
status: 'Qualified',
|
||||||
|
|
||||||
// type code here for "relation_one" field
|
// type code here for "relation_one" field
|
||||||
|
|
||||||
@ -185,7 +193,7 @@ const LeadsData = [
|
|||||||
{
|
{
|
||||||
name: 'Beta LLC',
|
name: 'Beta LLC',
|
||||||
|
|
||||||
status: 'Contacted',
|
status: 'Lost',
|
||||||
|
|
||||||
// type code here for "relation_one" field
|
// type code here for "relation_one" field
|
||||||
|
|
||||||
@ -195,7 +203,7 @@ const LeadsData = [
|
|||||||
{
|
{
|
||||||
name: 'Gamma Inc',
|
name: 'Gamma Inc',
|
||||||
|
|
||||||
status: 'Contacted',
|
status: 'Qualified',
|
||||||
|
|
||||||
// type code here for "relation_one" field
|
// type code here for "relation_one" field
|
||||||
|
|
||||||
@ -211,6 +219,16 @@ const LeadsData = [
|
|||||||
|
|
||||||
// type code here for "relation_one" field
|
// type code here for "relation_one" field
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'Epsilon Co',
|
||||||
|
|
||||||
|
status: 'Contacted',
|
||||||
|
|
||||||
|
// type code here for "relation_one" field
|
||||||
|
|
||||||
|
// type code here for "relation_one" field
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Similar logic for "relation_many"
|
// Similar logic for "relation_many"
|
||||||
@ -259,6 +277,17 @@ async function associateBookingWithUser() {
|
|||||||
if (Booking3?.setUser) {
|
if (Booking3?.setUser) {
|
||||||
await Booking3.setUser(relatedUser3);
|
await Booking3.setUser(relatedUser3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relatedUser4 = await Users.findOne({
|
||||||
|
offset: Math.floor(Math.random() * (await Users.count())),
|
||||||
|
});
|
||||||
|
const Booking4 = await Bookings.findOne({
|
||||||
|
order: [['id', 'ASC']],
|
||||||
|
offset: 4,
|
||||||
|
});
|
||||||
|
if (Booking4?.setUser) {
|
||||||
|
await Booking4.setUser(relatedUser4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function associateContactWithOwner() {
|
async function associateContactWithOwner() {
|
||||||
@ -305,6 +334,17 @@ async function associateContactWithOwner() {
|
|||||||
if (Contact3?.setOwner) {
|
if (Contact3?.setOwner) {
|
||||||
await Contact3.setOwner(relatedOwner3);
|
await Contact3.setOwner(relatedOwner3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relatedOwner4 = await Users.findOne({
|
||||||
|
offset: Math.floor(Math.random() * (await Users.count())),
|
||||||
|
});
|
||||||
|
const Contact4 = await Contacts.findOne({
|
||||||
|
order: [['id', 'ASC']],
|
||||||
|
offset: 4,
|
||||||
|
});
|
||||||
|
if (Contact4?.setOwner) {
|
||||||
|
await Contact4.setOwner(relatedOwner4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Similar logic for "relation_many"
|
// Similar logic for "relation_many"
|
||||||
@ -353,6 +393,17 @@ async function associateLeadWithOwner() {
|
|||||||
if (Lead3?.setOwner) {
|
if (Lead3?.setOwner) {
|
||||||
await Lead3.setOwner(relatedOwner3);
|
await Lead3.setOwner(relatedOwner3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relatedOwner4 = await Users.findOne({
|
||||||
|
offset: Math.floor(Math.random() * (await Users.count())),
|
||||||
|
});
|
||||||
|
const Lead4 = await Leads.findOne({
|
||||||
|
order: [['id', 'ASC']],
|
||||||
|
offset: 4,
|
||||||
|
});
|
||||||
|
if (Lead4?.setOwner) {
|
||||||
|
await Lead4.setOwner(relatedOwner4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function associateLeadWithCategory() {
|
async function associateLeadWithCategory() {
|
||||||
@ -399,6 +450,17 @@ async function associateLeadWithCategory() {
|
|||||||
if (Lead3?.setCategory) {
|
if (Lead3?.setCategory) {
|
||||||
await Lead3.setCategory(relatedCategory3);
|
await Lead3.setCategory(relatedCategory3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const relatedCategory4 = await Categories.findOne({
|
||||||
|
offset: Math.floor(Math.random() * (await Categories.count())),
|
||||||
|
});
|
||||||
|
const Lead4 = await Leads.findOne({
|
||||||
|
order: [['id', 'ASC']],
|
||||||
|
offset: 4,
|
||||||
|
});
|
||||||
|
if (Lead4?.setCategory) {
|
||||||
|
await Lead4.setCategory(relatedCategory4);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
|||||||
@ -1,12 +0,0 @@
|
|||||||
// List of Fiji public holidays (YYYY-MM-DD)
|
|
||||||
module.exports = [
|
|
||||||
'2024-01-01', // New Year's Day
|
|
||||||
'2024-01-02', // Day after New Year's Day
|
|
||||||
'2024-04-14', // Good Friday
|
|
||||||
'2024-04-17', // Easter Monday
|
|
||||||
'2024-05-01', // Labour Day
|
|
||||||
'2024-10-10', // Fiji Day
|
|
||||||
'2024-12-25', // Christmas Day
|
|
||||||
'2024-12-26', // Boxing Day
|
|
||||||
// Add more dates as needed
|
|
||||||
];
|
|
||||||
@ -8,9 +8,6 @@ const router = express.Router();
|
|||||||
|
|
||||||
const { parse } = require('json2csv');
|
const { parse } = require('json2csv');
|
||||||
|
|
||||||
const moment = require('moment');
|
|
||||||
const holidays = require('../holidays');
|
|
||||||
const ValidationError = require('../services/notifications/errors/validation');
|
|
||||||
const { checkCrudPermissions } = require('../middlewares/check-permissions');
|
const { checkCrudPermissions } = require('../middlewares/check-permissions');
|
||||||
|
|
||||||
router.use(checkCrudPermissions('bookings'));
|
router.use(checkCrudPermissions('bookings'));
|
||||||
@ -26,15 +23,6 @@ router.use(checkCrudPermissions('bookings'));
|
|||||||
* title:
|
* title:
|
||||||
* type: string
|
* type: string
|
||||||
* default: title
|
* default: title
|
||||||
* vehiclenumber:
|
|
||||||
* type: string
|
|
||||||
* default: vehiclenumber
|
|
||||||
* phone:
|
|
||||||
* type: string
|
|
||||||
* default: phone
|
|
||||||
* email:
|
|
||||||
* type: string
|
|
||||||
* default: email
|
|
||||||
|
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@ -85,28 +73,6 @@ router.post(
|
|||||||
const referer =
|
const referer =
|
||||||
req.headers.referer ||
|
req.headers.referer ||
|
||||||
`${req.protocol}://${req.hostname}${req.originalUrl}`;
|
`${req.protocol}://${req.hostname}${req.originalUrl}`;
|
||||||
// Booking validations
|
|
||||||
const { start_date, vehiclenumber, phone, email } = req.body.data;
|
|
||||||
if (!vehiclenumber) {
|
|
||||||
throw new ValidationError('vehicleNumberRequired', 'Vehicle number is required.');
|
|
||||||
}
|
|
||||||
if (!phone && !email) {
|
|
||||||
throw new ValidationError('contactInfoRequired', 'Either phone or email must be provided.');
|
|
||||||
}
|
|
||||||
const date = moment(start_date);
|
|
||||||
const day = date.day(); // Sunday = 0, Saturday = 6
|
|
||||||
const dateStr = date.format('YYYY-MM-DD');
|
|
||||||
if (day === 0) {
|
|
||||||
throw new ValidationError('noSundayBookings', 'Bookings cannot be made on Sundays.');
|
|
||||||
}
|
|
||||||
if (holidays.includes(dateStr)) {
|
|
||||||
throw new ValidationError('noHolidayBookings', 'Bookings cannot be made on public holidays.');
|
|
||||||
}
|
|
||||||
if (day === 6 && date.hour() > 13) {
|
|
||||||
throw new ValidationError('saturdayCutoff', 'Saturday bookings are only allowed until 1 PM.');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const link = new URL(referer);
|
const link = new URL(referer);
|
||||||
await BookingsService.create(
|
await BookingsService.create(
|
||||||
req.body.data,
|
req.body.data,
|
||||||
@ -339,16 +305,7 @@ router.get(
|
|||||||
const currentUser = req.currentUser;
|
const currentUser = req.currentUser;
|
||||||
const payload = await BookingsDBApi.findAll(req.query, { currentUser });
|
const payload = await BookingsDBApi.findAll(req.query, { currentUser });
|
||||||
if (filetype && filetype === 'csv') {
|
if (filetype && filetype === 'csv') {
|
||||||
const fields = [
|
const fields = ['id', 'title', 'start_date', 'end_date'];
|
||||||
'id',
|
|
||||||
'title',
|
|
||||||
'vehiclenumber',
|
|
||||||
'phone',
|
|
||||||
'email',
|
|
||||||
|
|
||||||
'start_date',
|
|
||||||
'end_date',
|
|
||||||
];
|
|
||||||
const opts = { fields };
|
const opts = { fields };
|
||||||
try {
|
try {
|
||||||
const csv = parse(payload.rows, opts);
|
const csv = parse(payload.rows, opts);
|
||||||
|
|||||||
@ -43,7 +43,7 @@ module.exports = class SearchService {
|
|||||||
const tableColumns = {
|
const tableColumns = {
|
||||||
users: ['firstName', 'lastName', 'phoneNumber', 'email'],
|
users: ['firstName', 'lastName', 'phoneNumber', 'email'],
|
||||||
|
|
||||||
bookings: ['title', 'vehiclenumber', 'phone', 'email'],
|
bookings: ['title'],
|
||||||
|
|
||||||
categories: ['name'],
|
categories: ['name'],
|
||||||
|
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
{}
|
|
||||||
@ -6,8 +6,7 @@ import {
|
|||||||
SlotInfo,
|
SlotInfo,
|
||||||
EventProps,
|
EventProps,
|
||||||
} from 'react-big-calendar';
|
} from 'react-big-calendar';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment';
|
||||||
moment.tz.setDefault('Etc/GMT+12');
|
|
||||||
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
import 'react-big-calendar/lib/css/react-big-calendar.css';
|
||||||
import ListActionsPopover from './ListActionsPopover';
|
import ListActionsPopover from './ListActionsPopover';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
@ -120,10 +119,6 @@ const BigCalendar = ({
|
|||||||
selectable={hasCreatePermission}
|
selectable={hasCreatePermission}
|
||||||
onSelectSlot={handleCreateEventAction}
|
onSelectSlot={handleCreateEventAction}
|
||||||
onRangeChange={onRangeChange}
|
onRangeChange={onRangeChange}
|
||||||
step={20}
|
|
||||||
timeslots={3}
|
|
||||||
min={new Date(1970, 1, 1, 8)}
|
|
||||||
max={new Date(1970, 1, 1, 16)}
|
|
||||||
scrollToTime={scrollToTime}
|
scrollToTime={scrollToTime}
|
||||||
components={{
|
components={{
|
||||||
event: (props) => (
|
event: (props) => (
|
||||||
|
|||||||
@ -124,31 +124,6 @@ const CardBookings = ({
|
|||||||
</div>
|
</div>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='flex justify-between gap-x-4 py-3'>
|
|
||||||
<dt className=' text-gray-500 dark:text-dark-600'>
|
|
||||||
Vehiclenumber
|
|
||||||
</dt>
|
|
||||||
<dd className='flex items-start gap-x-2'>
|
|
||||||
<div className='font-medium line-clamp-4'>
|
|
||||||
{item.vehiclenumber}
|
|
||||||
</div>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-between gap-x-4 py-3'>
|
|
||||||
<dt className=' text-gray-500 dark:text-dark-600'>Phone</dt>
|
|
||||||
<dd className='flex items-start gap-x-2'>
|
|
||||||
<div className='font-medium line-clamp-4'>{item.phone}</div>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='flex justify-between gap-x-4 py-3'>
|
|
||||||
<dt className=' text-gray-500 dark:text-dark-600'>Email</dt>
|
|
||||||
<dd className='flex items-start gap-x-2'>
|
|
||||||
<div className='font-medium line-clamp-4'>{item.email}</div>
|
|
||||||
</dd>
|
|
||||||
</div>
|
|
||||||
</dl>
|
</dl>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -81,23 +81,6 @@ const ListBookings = ({
|
|||||||
<p className={'text-xs text-gray-500 '}>Status</p>
|
<p className={'text-xs text-gray-500 '}>Status</p>
|
||||||
<p className={'line-clamp-2'}>{item.status}</p>
|
<p className={'line-clamp-2'}>{item.status}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'flex-1 px-3'}>
|
|
||||||
<p className={'text-xs text-gray-500 '}>
|
|
||||||
Vehiclenumber
|
|
||||||
</p>
|
|
||||||
<p className={'line-clamp-2'}>{item.vehiclenumber}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'flex-1 px-3'}>
|
|
||||||
<p className={'text-xs text-gray-500 '}>Phone</p>
|
|
||||||
<p className={'line-clamp-2'}>{item.phone}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'flex-1 px-3'}>
|
|
||||||
<p className={'text-xs text-gray-500 '}>Email</p>
|
|
||||||
<p className={'line-clamp-2'}>{item.email}</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
<ListActionsPopover
|
<ListActionsPopover
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
|
|||||||
@ -114,42 +114,6 @@ export const loadColumns = async (
|
|||||||
editable: hasUpdatePermission,
|
editable: hasUpdatePermission,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
|
||||||
field: 'vehiclenumber',
|
|
||||||
headerName: 'Vehiclenumber',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'phone',
|
|
||||||
headerName: 'Phone',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
field: 'email',
|
|
||||||
headerName: 'Email',
|
|
||||||
flex: 1,
|
|
||||||
minWidth: 120,
|
|
||||||
filterable: false,
|
|
||||||
headerClassName: 'datagrid--header',
|
|
||||||
cellClassName: 'datagrid--cell',
|
|
||||||
|
|
||||||
editable: hasUpdatePermission,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
{
|
||||||
field: 'actions',
|
field: 'actions',
|
||||||
type: 'actions',
|
type: 'actions',
|
||||||
|
|||||||
@ -20,15 +20,3 @@ export const getPageTitle = (currentPageTitle: string) =>
|
|||||||
`${currentPageTitle} — ${appTitle}`;
|
`${currentPageTitle} — ${appTitle}`;
|
||||||
|
|
||||||
export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || '';
|
export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || '';
|
||||||
|
|
||||||
// Fiji public holidays (YYYY-MM-DD)
|
|
||||||
export const holidays: string[] = [
|
|
||||||
'2024-01-01', // New Year's Day
|
|
||||||
'2024-01-02', // Day after New Year's Day
|
|
||||||
'2024-04-14', // Good Friday
|
|
||||||
'2024-04-17', // Easter Monday
|
|
||||||
'2024-05-01', // Labour Day
|
|
||||||
'2024-10-10', // Fiji Day
|
|
||||||
'2024-12-25', // Christmas Day
|
|
||||||
'2024-12-26', // Boxing Day
|
|
||||||
];
|
|
||||||
|
|||||||
@ -45,12 +45,6 @@ const EditBookings = () => {
|
|||||||
user: null,
|
user: null,
|
||||||
|
|
||||||
status: '',
|
status: '',
|
||||||
|
|
||||||
vehiclenumber: '',
|
|
||||||
|
|
||||||
phone: '',
|
|
||||||
|
|
||||||
email: '',
|
|
||||||
};
|
};
|
||||||
const [initialValues, setInitialValues] = useState(initVals);
|
const [initialValues, setInitialValues] = useState(initVals);
|
||||||
|
|
||||||
@ -166,18 +160,6 @@ const EditBookings = () => {
|
|||||||
</Field>
|
</Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label='Vehiclenumber'>
|
|
||||||
<Field name='vehiclenumber' placeholder='Vehiclenumber' />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField label='Phone'>
|
|
||||||
<Field name='phone' placeholder='Phone' />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField label='Email'>
|
|
||||||
<Field name='email' placeholder='Email' />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type='submit' color='info' label='Submit' />
|
<BaseButton type='submit' color='info' label='Submit' />
|
||||||
|
|||||||
@ -45,12 +45,6 @@ const EditBookingsPage = () => {
|
|||||||
user: null,
|
user: null,
|
||||||
|
|
||||||
status: '',
|
status: '',
|
||||||
|
|
||||||
vehiclenumber: '',
|
|
||||||
|
|
||||||
phone: '',
|
|
||||||
|
|
||||||
email: '',
|
|
||||||
};
|
};
|
||||||
const [initialValues, setInitialValues] = useState(initVals);
|
const [initialValues, setInitialValues] = useState(initVals);
|
||||||
|
|
||||||
@ -164,18 +158,6 @@ const EditBookingsPage = () => {
|
|||||||
</Field>
|
</Field>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label='Vehiclenumber'>
|
|
||||||
<Field name='vehiclenumber' placeholder='Vehiclenumber' />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField label='Phone'>
|
|
||||||
<Field name='phone' placeholder='Phone' />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<FormField label='Email'>
|
|
||||||
<Field name='email' placeholder='Email' />
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type='submit' color='info' label='Submit' />
|
<BaseButton type='submit' color='info' label='Submit' />
|
||||||
|
|||||||
@ -30,9 +30,6 @@ const BookingsTablesPage = () => {
|
|||||||
|
|
||||||
const [filters] = useState([
|
const [filters] = useState([
|
||||||
{ label: 'Title', title: 'title' },
|
{ label: 'Title', title: 'title' },
|
||||||
{ label: 'Vehiclenumber', title: 'vehiclenumber' },
|
|
||||||
{ label: 'Phone', title: 'phone' },
|
|
||||||
{ label: 'Email', title: 'email' },
|
|
||||||
|
|
||||||
{ label: 'StartDate', title: 'start_date', date: 'true' },
|
{ label: 'StartDate', title: 'start_date', date: 'true' },
|
||||||
{ label: 'EndDate', title: 'end_date', date: 'true' },
|
{ label: 'EndDate', title: 'end_date', date: 'true' },
|
||||||
|
|||||||
@ -4,11 +4,9 @@ import {
|
|||||||
mdiMail,
|
mdiMail,
|
||||||
mdiUpload,
|
mdiUpload,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { holidays } from '../../config';
|
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import React, { ReactElement } from 'react';
|
import React, { ReactElement } from 'react';
|
||||||
import CardBox from '../../components/CardBox';
|
import CardBox from '../../components/CardBox';
|
||||||
import LayoutGuest from '../../layouts/Guest';
|
|
||||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||||
import SectionMain from '../../components/SectionMain';
|
import SectionMain from '../../components/SectionMain';
|
||||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||||
@ -35,11 +33,15 @@ import { useRouter } from 'next/router';
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
date: '',
|
title: '',
|
||||||
timeSlot: '',
|
|
||||||
vehiclenumber: '',
|
start_date: '',
|
||||||
phone: '',
|
|
||||||
email: '',
|
end_date: '',
|
||||||
|
|
||||||
|
user: '',
|
||||||
|
|
||||||
|
status: 'Pending',
|
||||||
};
|
};
|
||||||
|
|
||||||
const BookingsNew = () => {
|
const BookingsNew = () => {
|
||||||
@ -67,65 +69,73 @@ const BookingsNew = () => {
|
|||||||
{''}
|
{''}
|
||||||
</SectionTitleLineWithButton>
|
</SectionTitleLineWithButton>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
|
<Formik
|
||||||
{({ values }) => {
|
initialValues={
|
||||||
// Generate 20-minute slots between 08:00 and 16:00
|
dateRangeStart && dateRangeEnd
|
||||||
const slots: string[] = [];
|
? {
|
||||||
if (values.date) {
|
...initialValues,
|
||||||
const date = moment(values.date);
|
start_date:
|
||||||
const day = date.day(); // Sunday = 0, Saturday = 6
|
moment(dateRangeStart).format('YYYY-MM-DDTHH:mm'),
|
||||||
const dateStr = date.format('YYYY-MM-DD');
|
end_date: moment(dateRangeEnd).format('YYYY-MM-DDTHH:mm'),
|
||||||
const startHour = 8;
|
|
||||||
const endHour = 16;
|
|
||||||
let cursor = date.clone().hour(startHour).minute(0);
|
|
||||||
while (cursor.hour() < endHour) {
|
|
||||||
const slot = cursor.format('HH:mm');
|
|
||||||
// Exclude Sundays and holidays
|
|
||||||
if (day !== 0 && !holidays.includes(dateStr)) {
|
|
||||||
// Saturday cutoff at 13:00
|
|
||||||
if (!(day === 6 && cursor.hour() > 13)) {
|
|
||||||
slots.push(slot);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
cursor = cursor.add(20, 'minutes');
|
: initialValues
|
||||||
}
|
}
|
||||||
}
|
onSubmit={(values) => handleSubmit(values)}
|
||||||
return (
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<FormField label="Date">
|
<FormField label='Title'>
|
||||||
<Field type="date" name="date" />
|
<Field name='title' placeholder='Title' />
|
||||||
</FormField>
|
</FormField>
|
||||||
<FormField label="Time Slot">
|
|
||||||
<Field
|
<FormField label='StartDate'>
|
||||||
component={SelectField}
|
<Field
|
||||||
name="timeSlot"
|
type='datetime-local'
|
||||||
options={slots.map((s) => ({ label: s, value: s }))}
|
name='start_date'
|
||||||
/>
|
placeholder='StartDate'
|
||||||
</FormField>
|
/>
|
||||||
<FormField label="Vehicle Number">
|
</FormField>
|
||||||
<Field name="vehiclenumber" placeholder="Vehicle Number" />
|
|
||||||
</FormField>
|
<FormField label='EndDate'>
|
||||||
<FormField label="Phone">
|
<Field
|
||||||
<Field name="phone" placeholder="Your phone" />
|
type='datetime-local'
|
||||||
</FormField>
|
name='end_date'
|
||||||
<FormField label="Email">
|
placeholder='EndDate'
|
||||||
<Field name="email" placeholder="Your email" />
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
<BaseDivider />
|
|
||||||
<BaseButtons>
|
<FormField label='User' labelFor='user'>
|
||||||
<BaseButton type="submit" color="info" label="Submit" />
|
<Field
|
||||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
name='user'
|
||||||
<BaseButton
|
id='user'
|
||||||
type="button"
|
component={SelectField}
|
||||||
color="danger"
|
options={[]}
|
||||||
outline
|
itemRef={'users'}
|
||||||
label="Cancel"
|
></Field>
|
||||||
onClick={() => router.push('/')}
|
</FormField>
|
||||||
/>
|
|
||||||
</BaseButtons>
|
<FormField label='Status' labelFor='status'>
|
||||||
</Form>
|
<Field name='status' id='status' component='select'>
|
||||||
);
|
<option value='Pending'>Pending</option>
|
||||||
}}
|
|
||||||
|
<option value='Inspected'>Inspected</option>
|
||||||
|
|
||||||
|
<option value='Cancelled'>Cancelled</option>
|
||||||
|
</Field>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<BaseDivider />
|
||||||
|
<BaseButtons>
|
||||||
|
<BaseButton type='submit' color='info' label='Submit' />
|
||||||
|
<BaseButton type='reset' color='info' outline label='Reset' />
|
||||||
|
<BaseButton
|
||||||
|
type='reset'
|
||||||
|
color='danger'
|
||||||
|
outline
|
||||||
|
label='Cancel'
|
||||||
|
onClick={() => router.push('/bookings/bookings-list')}
|
||||||
|
/>
|
||||||
|
</BaseButtons>
|
||||||
|
</Form>
|
||||||
</Formik>
|
</Formik>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</SectionMain>
|
</SectionMain>
|
||||||
@ -135,9 +145,9 @@ const BookingsNew = () => {
|
|||||||
|
|
||||||
BookingsNew.getLayout = function getLayout(page: ReactElement) {
|
BookingsNew.getLayout = function getLayout(page: ReactElement) {
|
||||||
return (
|
return (
|
||||||
<LayoutGuest>
|
<LayoutAuthenticated permission={'CREATE_BOOKINGS'}>
|
||||||
{page}
|
{page}
|
||||||
</LayoutGuest>
|
</LayoutAuthenticated>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -30,9 +30,6 @@ const BookingsTablesPage = () => {
|
|||||||
|
|
||||||
const [filters] = useState([
|
const [filters] = useState([
|
||||||
{ label: 'Title', title: 'title' },
|
{ label: 'Title', title: 'title' },
|
||||||
{ label: 'Vehiclenumber', title: 'vehiclenumber' },
|
|
||||||
{ label: 'Phone', title: 'phone' },
|
|
||||||
{ label: 'Email', title: 'email' },
|
|
||||||
|
|
||||||
{ label: 'StartDate', title: 'start_date', date: 'true' },
|
{ label: 'StartDate', title: 'start_date', date: 'true' },
|
||||||
{ label: 'EndDate', title: 'end_date', date: 'true' },
|
{ label: 'EndDate', title: 'end_date', date: 'true' },
|
||||||
|
|||||||
@ -108,21 +108,6 @@ const BookingsView = () => {
|
|||||||
<p>{bookings?.status ?? 'No data'}</p>
|
<p>{bookings?.status ?? 'No data'}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Vehiclenumber</p>
|
|
||||||
<p>{bookings?.vehiclenumber}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Phone</p>
|
|
||||||
<p>{bookings?.phone}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={'mb-4'}>
|
|
||||||
<p className={'block font-bold mb-2'}>Email</p>
|
|
||||||
<p>{bookings?.email}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
|
|
||||||
<BaseButton
|
<BaseButton
|
||||||
|
|||||||
@ -96,7 +96,7 @@ export default function WebSite() {
|
|||||||
<FeaturesSection
|
<FeaturesSection
|
||||||
projectName={'Vehicle Inspection Booking System'}
|
projectName={'Vehicle Inspection Booking System'}
|
||||||
image={['Dashboard showcasing CRM features']}
|
image={['Dashboard showcasing CRM features']}
|
||||||
withBg={0}
|
withBg={1}
|
||||||
features={features_points}
|
features={features_points}
|
||||||
mainText={`Discover Key Features of ${projectName}`}
|
mainText={`Discover Key Features of ${projectName}`}
|
||||||
subTitle={`Unlock the full potential of your legal operations with ${projectName}. Streamline processes, enhance collaboration, and boost productivity.`}
|
subTitle={`Unlock the full potential of your legal operations with ${projectName}. Streamline processes, enhance collaboration, and boost productivity.`}
|
||||||
|
|||||||
@ -155,12 +155,6 @@ const UsersView = () => {
|
|||||||
<th>EndDate</th>
|
<th>EndDate</th>
|
||||||
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
|
|
||||||
<th>Vehiclenumber</th>
|
|
||||||
|
|
||||||
<th>Phone</th>
|
|
||||||
|
|
||||||
<th>Email</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -186,14 +180,6 @@ const UsersView = () => {
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td data-label='status'>{item.status}</td>
|
<td data-label='status'>{item.status}</td>
|
||||||
|
|
||||||
<td data-label='vehiclenumber'>
|
|
||||||
{item.vehiclenumber}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td data-label='phone'>{item.phone}</td>
|
|
||||||
|
|
||||||
<td data-label='email'>{item.email}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@ -110,7 +110,7 @@ export default function WebSite() {
|
|||||||
<FeaturesSection
|
<FeaturesSection
|
||||||
projectName={'Vehicle Inspection Booking System'}
|
projectName={'Vehicle Inspection Booking System'}
|
||||||
image={['Icons representing CRM features']}
|
image={['Icons representing CRM features']}
|
||||||
withBg={0}
|
withBg={1}
|
||||||
features={features_points}
|
features={features_points}
|
||||||
mainText={`Unleash the Power of ${projectName}`}
|
mainText={`Unleash the Power of ${projectName}`}
|
||||||
subTitle={`Explore the robust features of ${projectName} designed to elevate your legal practice. Enhance productivity, streamline operations, and drive success.`}
|
subTitle={`Explore the robust features of ${projectName} designed to elevate your legal practice. Enhance productivity, streamline operations, and drive success.`}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user