version 1.2

This commit is contained in:
Flatlogic Bot 2025-08-15 19:54:24 +00:00
parent 0b3a035811
commit 1387fa9cf0
23 changed files with 511 additions and 173 deletions

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,9 @@ module.exports = class BookingsDBApi {
start_date: data.start_date || null,
end_date: data.end_date || null,
status: data.status || null,
vehiclenumber: data.vehiclenumber || null,
phone: data.phone || null,
email: data.email || null,
importHash: data.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@ -45,6 +48,9 @@ module.exports = class BookingsDBApi {
start_date: item.start_date || null,
end_date: item.end_date || null,
status: item.status || null,
vehiclenumber: item.vehiclenumber || null,
phone: item.phone || null,
email: item.email || null,
importHash: item.importHash || null,
createdById: currentUser.id,
updatedById: currentUser.id,
@ -78,6 +84,13 @@ module.exports = class BookingsDBApi {
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;
await bookings.update(updatePayload, { transaction });
@ -213,6 +226,31 @@ 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) {
where = {
...where,

View File

@ -0,0 +1,49 @@
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;
}
},
};

View File

@ -0,0 +1,47 @@
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;
}
},
};

View File

@ -0,0 +1,47 @@
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;
}
},
};

View File

@ -32,6 +32,18 @@ module.exports = function (sequelize, DataTypes) {
values: ['Pending', 'Inspected', 'Cancelled'],
},
vehiclenumber: {
type: DataTypes.TEXT,
},
phone: {
type: DataTypes.TEXT,
},
email: {
type: DataTypes.TEXT,
},
importHash: {
type: DataTypes.STRING(255),
allowNull: true,

View File

@ -22,6 +22,12 @@ const BookingsData = [
// type code here for "relation_one" field
status: 'Inspected',
vehiclenumber: 'Emil Kraepelin',
phone: 'Lynn Margulis',
email: 'Sigmund Freud',
},
{
@ -33,7 +39,13 @@ const BookingsData = [
// type code here for "relation_one" field
status: 'Inspected',
status: 'Pending',
vehiclenumber: 'Max Planck',
phone: 'Tycho Brahe',
email: 'Francis Galton',
},
{
@ -45,7 +57,13 @@ const BookingsData = [
// type code here for "relation_one" field
status: 'Inspected',
status: 'Cancelled',
vehiclenumber: 'Emil Fischer',
phone: 'Francis Crick',
email: 'Jean Baptiste Lamarck',
},
{
@ -57,19 +75,13 @@ const BookingsData = [
// 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',
vehiclenumber: 'Nicolaus Copernicus',
phone: 'Charles Sherrington',
email: 'Ernest Rutherford',
},
];
@ -89,10 +101,6 @@ const CategoriesData = [
{
name: 'Startup',
},
{
name: 'Non-Profit',
},
];
const ContactsData = [
@ -135,16 +143,6 @@ const ContactsData = [
// 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 = [
@ -171,19 +169,13 @@ const DepartmentsData = [
// type code here for "relation_many" field
},
{
name: 'IT',
// type code here for "relation_many" field
},
];
const LeadsData = [
{
name: 'Acme Corp',
status: 'Qualified',
status: 'Contacted',
// type code here for "relation_one" field
@ -193,7 +185,7 @@ const LeadsData = [
{
name: 'Beta LLC',
status: 'Lost',
status: 'Contacted',
// type code here for "relation_one" field
@ -203,7 +195,7 @@ const LeadsData = [
{
name: 'Gamma Inc',
status: 'Qualified',
status: 'Contacted',
// type code here for "relation_one" field
@ -219,16 +211,6 @@ const LeadsData = [
// 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"
@ -277,17 +259,6 @@ async function associateBookingWithUser() {
if (Booking3?.setUser) {
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() {
@ -334,17 +305,6 @@ async function associateContactWithOwner() {
if (Contact3?.setOwner) {
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"
@ -393,17 +353,6 @@ async function associateLeadWithOwner() {
if (Lead3?.setOwner) {
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() {
@ -450,17 +399,6 @@ async function associateLeadWithCategory() {
if (Lead3?.setCategory) {
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 = {

12
backend/src/holidays.js Normal file
View File

@ -0,0 +1,12 @@
// 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
];

View File

@ -8,6 +8,9 @@ const router = express.Router();
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');
router.use(checkCrudPermissions('bookings'));
@ -23,6 +26,15 @@ router.use(checkCrudPermissions('bookings'));
* title:
* type: string
* default: title
* vehiclenumber:
* type: string
* default: vehiclenumber
* phone:
* type: string
* default: phone
* email:
* type: string
* default: email
*
*/
@ -73,6 +85,28 @@ router.post(
const referer =
req.headers.referer ||
`${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);
await BookingsService.create(
req.body.data,
@ -305,7 +339,16 @@ router.get(
const currentUser = req.currentUser;
const payload = await BookingsDBApi.findAll(req.query, { currentUser });
if (filetype && filetype === 'csv') {
const fields = ['id', 'title', 'start_date', 'end_date'];
const fields = [
'id',
'title',
'vehiclenumber',
'phone',
'email',
'start_date',
'end_date',
];
const opts = { fields };
try {
const csv = parse(payload.rows, opts);

View File

@ -43,7 +43,7 @@ module.exports = class SearchService {
const tableColumns = {
users: ['firstName', 'lastName', 'phoneNumber', 'email'],
bookings: ['title'],
bookings: ['title', 'vehiclenumber', 'phone', 'email'],
categories: ['name'],

View File

@ -124,6 +124,31 @@ const CardBookings = ({
</div>
</dd>
</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>
</li>
))}

View File

@ -81,6 +81,23 @@ const ListBookings = ({
<p className={'text-xs text-gray-500 '}>Status</p>
<p className={'line-clamp-2'}>{item.status}</p>
</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>
<ListActionsPopover
onDelete={onDelete}

View File

@ -114,6 +114,42 @@ export const loadColumns = async (
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',
type: 'actions',

View File

@ -20,3 +20,15 @@ export const getPageTitle = (currentPageTitle: string) =>
`${currentPageTitle}${appTitle}`;
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
];

View File

@ -45,6 +45,12 @@ const EditBookings = () => {
user: null,
status: '',
vehiclenumber: '',
phone: '',
email: '',
};
const [initialValues, setInitialValues] = useState(initVals);
@ -160,6 +166,18 @@ const EditBookings = () => {
</Field>
</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 />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />

View File

@ -45,6 +45,12 @@ const EditBookingsPage = () => {
user: null,
status: '',
vehiclenumber: '',
phone: '',
email: '',
};
const [initialValues, setInitialValues] = useState(initVals);
@ -158,6 +164,18 @@ const EditBookingsPage = () => {
</Field>
</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 />
<BaseButtons>
<BaseButton type='submit' color='info' label='Submit' />

View File

@ -30,6 +30,9 @@ const BookingsTablesPage = () => {
const [filters] = useState([
{ label: 'Title', title: 'title' },
{ label: 'Vehiclenumber', title: 'vehiclenumber' },
{ label: 'Phone', title: 'phone' },
{ label: 'Email', title: 'email' },
{ label: 'StartDate', title: 'start_date', date: 'true' },
{ label: 'EndDate', title: 'end_date', date: 'true' },

View File

@ -4,9 +4,11 @@ import {
mdiMail,
mdiUpload,
} from '@mdi/js';
import { holidays } from '../../config';
import Head from 'next/head';
import React, { ReactElement } from 'react';
import CardBox from '../../components/CardBox';
import LayoutGuest from '../../layouts/Guest';
import LayoutAuthenticated from '../../layouts/Authenticated';
import SectionMain from '../../components/SectionMain';
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
@ -33,15 +35,11 @@ import { useRouter } from 'next/router';
import moment from 'moment';
const initialValues = {
title: '',
start_date: '',
end_date: '',
user: '',
status: 'Pending',
date: '',
timeSlot: '',
vehiclenumber: '',
phone: '',
email: '',
};
const BookingsNew = () => {
@ -69,73 +67,65 @@ const BookingsNew = () => {
{''}
</SectionTitleLineWithButton>
<CardBox>
<Formik
initialValues={
dateRangeStart && dateRangeEnd
? {
...initialValues,
start_date:
moment(dateRangeStart).format('YYYY-MM-DDTHH:mm'),
end_date: moment(dateRangeEnd).format('YYYY-MM-DDTHH:mm'),
<Formik initialValues={initialValues} onSubmit={handleSubmit}>
{({ values }) => {
// Generate 20-minute slots between 08:00 and 16:00
const slots: string[] = [];
if (values.date) {
const date = moment(values.date);
const day = date.day(); // Sunday = 0, Saturday = 6
const dateStr = date.format('YYYY-MM-DD');
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);
}
}
: initialValues
}
onSubmit={(values) => handleSubmit(values)}
>
<Form>
<FormField label='Title'>
<Field name='title' placeholder='Title' />
</FormField>
<FormField label='StartDate'>
<Field
type='datetime-local'
name='start_date'
placeholder='StartDate'
/>
</FormField>
<FormField label='EndDate'>
<Field
type='datetime-local'
name='end_date'
placeholder='EndDate'
/>
</FormField>
<FormField label='User' labelFor='user'>
<Field
name='user'
id='user'
component={SelectField}
options={[]}
itemRef={'users'}
></Field>
</FormField>
<FormField label='Status' labelFor='status'>
<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>
cursor = cursor.add(20, 'minutes');
}
}
return (
<Form>
<FormField label="Date">
<Field type="date" name="date" />
</FormField>
<FormField label="Time Slot">
<Field
component={SelectField}
name="timeSlot"
options={slots.map((s) => ({ label: s, value: s }))}
/>
</FormField>
<FormField label="Vehicle Number">
<Field name="vehiclenumber" placeholder="Vehicle Number" />
</FormField>
<FormField label="Phone">
<Field name="phone" placeholder="Your phone" />
</FormField>
<FormField label="Email">
<Field name="email" placeholder="Your email" />
</FormField>
<BaseDivider />
<BaseButtons>
<BaseButton type="submit" color="info" label="Submit" />
<BaseButton type="reset" color="info" outline label="Reset" />
<BaseButton
type="button"
color="danger"
outline
label="Cancel"
onClick={() => router.push('/')}
/>
</BaseButtons>
</Form>
);
}}
</Formik>
</CardBox>
</SectionMain>
@ -145,9 +135,9 @@ const BookingsNew = () => {
BookingsNew.getLayout = function getLayout(page: ReactElement) {
return (
<LayoutAuthenticated permission={'CREATE_BOOKINGS'}>
<LayoutGuest>
{page}
</LayoutAuthenticated>
</LayoutGuest>
);
};

View File

@ -30,6 +30,9 @@ const BookingsTablesPage = () => {
const [filters] = useState([
{ label: 'Title', title: 'title' },
{ label: 'Vehiclenumber', title: 'vehiclenumber' },
{ label: 'Phone', title: 'phone' },
{ label: 'Email', title: 'email' },
{ label: 'StartDate', title: 'start_date', date: 'true' },
{ label: 'EndDate', title: 'end_date', date: 'true' },

View File

@ -108,6 +108,21 @@ const BookingsView = () => {
<p>{bookings?.status ?? 'No data'}</p>
</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 />
<BaseButton

View File

@ -96,7 +96,7 @@ export default function WebSite() {
<FeaturesSection
projectName={'Vehicle Inspection Booking System'}
image={['Dashboard showcasing CRM features']}
withBg={1}
withBg={0}
features={features_points}
mainText={`Discover Key Features of ${projectName}`}
subTitle={`Unlock the full potential of your legal operations with ${projectName}. Streamline processes, enhance collaboration, and boost productivity.`}

View File

@ -155,6 +155,12 @@ const UsersView = () => {
<th>EndDate</th>
<th>Status</th>
<th>Vehiclenumber</th>
<th>Phone</th>
<th>Email</th>
</tr>
</thead>
<tbody>
@ -180,6 +186,14 @@ const UsersView = () => {
</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>
))}
</tbody>

View File

@ -110,7 +110,7 @@ export default function WebSite() {
<FeaturesSection
projectName={'Vehicle Inspection Booking System'}
image={['Icons representing CRM features']}
withBg={1}
withBg={0}
features={features_points}
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.`}