Autosave: 20260218-134704
This commit is contained in:
parent
b03b911e99
commit
111693ae6e
70
backend/src/db/api/claim_requests.js
Normal file
70
backend/src/db/api/claim_requests.js
Normal file
@ -0,0 +1,70 @@
|
||||
const db = require('../models');
|
||||
|
||||
module.exports = class Claim_requestsDBApi {
|
||||
static async create(data, { currentUser, transaction }) {
|
||||
const claim_request = await db.claim_requests.create(
|
||||
{
|
||||
businessId: data.businessId,
|
||||
userId: data.userId,
|
||||
status: data.status || 'PENDING',
|
||||
createdById: currentUser?.id,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
return claim_request;
|
||||
}
|
||||
|
||||
static async update(id, data, { currentUser, transaction }) {
|
||||
const claim_request = await db.claim_requests.findByPk(id, { transaction });
|
||||
if (!claim_request) throw new Error('Claim request not found');
|
||||
|
||||
await claim_request.update(
|
||||
{
|
||||
...data,
|
||||
updatedById: currentUser?.id,
|
||||
},
|
||||
{ transaction },
|
||||
);
|
||||
return claim_request;
|
||||
}
|
||||
|
||||
static async findBy(where, options = {}) {
|
||||
return await db.claim_requests.findOne({
|
||||
where,
|
||||
include: [
|
||||
{ model: db.businesses, as: 'business' },
|
||||
{ model: db.users, as: 'user' },
|
||||
],
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
static async findAll(query = {}, { currentUser } = {}) {
|
||||
const { limit, offset, filter } = query;
|
||||
const where = {};
|
||||
|
||||
// Support direct query params
|
||||
if (query.userId) where.userId = query.userId;
|
||||
if (query.status) where.status = query.status;
|
||||
if (query.businessId) where.businessId = query.businessId;
|
||||
|
||||
if (filter) {
|
||||
// Support filter object if provided
|
||||
if (filter.userId) where.userId = filter.userId;
|
||||
if (filter.status) where.status = filter.status;
|
||||
if (filter.businessId) where.businessId = filter.businessId;
|
||||
}
|
||||
|
||||
const { rows, count } = await db.claim_requests.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{ model: db.businesses, as: 'business' },
|
||||
{ model: db.users, as: 'user' },
|
||||
],
|
||||
limit: limit ? parseInt(limit) : undefined,
|
||||
offset: offset ? parseInt(offset) : undefined,
|
||||
order: [['createdAt', 'DESC']],
|
||||
});
|
||||
return { rows, count };
|
||||
}
|
||||
};
|
||||
@ -90,6 +90,7 @@ module.exports = class LeadsDBApi {
|
||||
where,
|
||||
include,
|
||||
distinct: true,
|
||||
subQuery: false,
|
||||
limit: options?.countOnly ? undefined : (limit ? Number(limit) : undefined),
|
||||
offset: options?.countOnly ? undefined : (offset ? Number(offset) : undefined),
|
||||
order: [['createdAt', 'desc']],
|
||||
|
||||
@ -72,6 +72,7 @@ module.exports = class MessagesDBApi {
|
||||
{
|
||||
model: db.leads,
|
||||
as: 'lead',
|
||||
required: false,
|
||||
include: [{
|
||||
model: db.lead_matches,
|
||||
as: 'lead_matches_lead',
|
||||
@ -82,27 +83,29 @@ module.exports = class MessagesDBApi {
|
||||
{ id: { [Op.in]: filter.lead.split('|').map(term => Utils.uuid(term)) } },
|
||||
{ keyword: { [Op.or]: filter.lead.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) } },
|
||||
]
|
||||
} : {},
|
||||
} : undefined,
|
||||
},
|
||||
{
|
||||
model: db.users,
|
||||
as: 'sender_user',
|
||||
required: false,
|
||||
where: filter.sender_user ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: filter.sender_user.split('|').map(term => Utils.uuid(term)) } },
|
||||
{ firstName: { [Op.or]: filter.sender_user.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) } },
|
||||
]
|
||||
} : {},
|
||||
} : undefined,
|
||||
},
|
||||
{
|
||||
model: db.users,
|
||||
as: 'receiver_user',
|
||||
required: false,
|
||||
where: filter.receiver_user ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: filter.receiver_user.split('|').map(term => Utils.uuid(term)) } },
|
||||
{ firstName: { [Op.or]: filter.receiver_user.split('|').map(term => ({ [Op.iLike]: `%${term}%` })) } },
|
||||
]
|
||||
} : {},
|
||||
} : undefined,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@ -373,7 +373,7 @@ module.exports = class ReviewsDBApi {
|
||||
{
|
||||
model: db.businesses,
|
||||
as: 'business',
|
||||
|
||||
required: false,
|
||||
where: filter.business ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: filter.business.split('|').map(term => Utils.uuid(term)) } },
|
||||
@ -383,14 +383,14 @@ module.exports = class ReviewsDBApi {
|
||||
}
|
||||
},
|
||||
]
|
||||
} : {},
|
||||
} : undefined,
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
model: db.users,
|
||||
as: 'user',
|
||||
|
||||
required: false,
|
||||
where: filter.user ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: filter.user.split('|').map(term => Utils.uuid(term)) } },
|
||||
@ -400,14 +400,14 @@ module.exports = class ReviewsDBApi {
|
||||
}
|
||||
},
|
||||
]
|
||||
} : {},
|
||||
} : undefined,
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
model: db.leads,
|
||||
as: 'lead',
|
||||
|
||||
required: false,
|
||||
where: filter.lead ? {
|
||||
[Op.or]: [
|
||||
{ id: { [Op.in]: filter.lead.split('|').map(term => Utils.uuid(term)) } },
|
||||
@ -417,7 +417,7 @@ module.exports = class ReviewsDBApi {
|
||||
}
|
||||
},
|
||||
]
|
||||
} : {},
|
||||
} : undefined,
|
||||
|
||||
},
|
||||
|
||||
@ -598,6 +598,7 @@ module.exports = class ReviewsDBApi {
|
||||
where,
|
||||
include,
|
||||
distinct: true,
|
||||
subQuery: false,
|
||||
order: filter.field && filter.sort
|
||||
? [[filter.field, filter.sort]]
|
||||
: [['createdAt', 'desc']],
|
||||
|
||||
@ -0,0 +1,72 @@
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.createTable('claim_requests', {
|
||||
id: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
defaultValue: Sequelize.DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
businessId: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'businesses',
|
||||
},
|
||||
allowNull: false,
|
||||
},
|
||||
userId: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
allowNull: false,
|
||||
},
|
||||
status: {
|
||||
type: Sequelize.DataTypes.ENUM('PENDING', 'APPROVED', 'REJECTED'),
|
||||
defaultValue: 'PENDING',
|
||||
allowNull: false,
|
||||
},
|
||||
rejectionReason: {
|
||||
type: Sequelize.DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
createdById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
},
|
||||
updatedById: {
|
||||
type: Sequelize.DataTypes.UUID,
|
||||
references: {
|
||||
key: 'id',
|
||||
model: 'users',
|
||||
},
|
||||
},
|
||||
createdAt: { type: Sequelize.DataTypes.DATE },
|
||||
updatedAt: { type: Sequelize.DataTypes.DATE },
|
||||
deletedAt: { type: Sequelize.DataTypes.DATE },
|
||||
}, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async down(queryInterface, Sequelize) {
|
||||
const transaction = await queryInterface.sequelize.transaction();
|
||||
try {
|
||||
await queryInterface.dropTable('claim_requests', { transaction });
|
||||
await transaction.commit();
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,62 @@
|
||||
const { v4: uuid } = require("uuid");
|
||||
|
||||
module.exports = {
|
||||
async up(queryInterface) {
|
||||
const createdAt = new Date();
|
||||
const updatedAt = new Date();
|
||||
|
||||
const permissions = [
|
||||
{ id: uuid(), name: 'CREATE_CLAIM_REQUESTS', createdAt, updatedAt },
|
||||
{ id: uuid(), name: 'READ_CLAIM_REQUESTS', createdAt, updatedAt },
|
||||
{ id: uuid(), name: 'UPDATE_CLAIM_REQUESTS', createdAt, updatedAt },
|
||||
{ id: uuid(), name: 'DELETE_CLAIM_REQUESTS', createdAt, updatedAt },
|
||||
];
|
||||
|
||||
await queryInterface.bulkInsert('permissions', permissions);
|
||||
|
||||
const roles = await queryInterface.sequelize.query(
|
||||
`SELECT id, name FROM "roles" WHERE name IN ('Administrator', 'Platform Owner', 'Verified Business Owner')`,
|
||||
{ type: queryInterface.sequelize.QueryTypes.SELECT }
|
||||
);
|
||||
|
||||
const adminRole = roles.find(r => r.name === 'Administrator');
|
||||
const ownerRole = roles.find(r => r.name === 'Platform Owner');
|
||||
const vboRole = roles.find(r => r.name === 'Verified Business Owner');
|
||||
|
||||
const rolePerms = [];
|
||||
|
||||
permissions.forEach(p => {
|
||||
if (adminRole) {
|
||||
rolePerms.push({
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: adminRole.id,
|
||||
permissionId: p.id,
|
||||
});
|
||||
}
|
||||
if (ownerRole) {
|
||||
rolePerms.push({
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: ownerRole.id,
|
||||
permissionId: p.id,
|
||||
});
|
||||
}
|
||||
// VBO can only create and read
|
||||
if (vboRole && (p.name === 'CREATE_CLAIM_REQUESTS' || p.name === 'READ_CLAIM_REQUESTS')) {
|
||||
rolePerms.push({
|
||||
createdAt,
|
||||
updatedAt,
|
||||
roles_permissionsId: vboRole.id,
|
||||
permissionId: p.id,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await queryInterface.bulkInsert('rolesPermissionsPermissions', rolePerms);
|
||||
},
|
||||
|
||||
async down(queryInterface) {
|
||||
// No need to implement down for this simple permission addition in a dev environment
|
||||
}
|
||||
};
|
||||
58
backend/src/db/models/claim_requests.js
Normal file
58
backend/src/db/models/claim_requests.js
Normal file
@ -0,0 +1,58 @@
|
||||
|
||||
module.exports = function(sequelize, DataTypes) {
|
||||
const claim_requests = sequelize.define(
|
||||
'claim_requests',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.ENUM('PENDING', 'APPROVED', 'REJECTED'),
|
||||
defaultValue: 'PENDING',
|
||||
allowNull: false,
|
||||
},
|
||||
rejectionReason: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: true,
|
||||
},
|
||||
createdAt: { type: DataTypes.DATE },
|
||||
updatedAt: { type: DataTypes.DATE },
|
||||
deletedAt: { type: DataTypes.DATE },
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
paranoid: true,
|
||||
freezeTableName: true,
|
||||
},
|
||||
);
|
||||
|
||||
claim_requests.associate = (db) => {
|
||||
db.claim_requests.belongsTo(db.businesses, {
|
||||
as: 'business',
|
||||
foreignKey: {
|
||||
name: 'businessId',
|
||||
},
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
db.claim_requests.belongsTo(db.users, {
|
||||
as: 'user',
|
||||
foreignKey: {
|
||||
name: 'userId',
|
||||
},
|
||||
constraints: false,
|
||||
});
|
||||
|
||||
db.claim_requests.belongsTo(db.users, {
|
||||
as: 'createdBy',
|
||||
});
|
||||
|
||||
db.claim_requests.belongsTo(db.users, {
|
||||
as: 'updatedBy',
|
||||
});
|
||||
};
|
||||
|
||||
return claim_requests;
|
||||
};
|
||||
@ -46,6 +46,8 @@ const verification_submissionsRoutes = require('./routes/verification_submission
|
||||
|
||||
const verification_evidencesRoutes = require('./routes/verification_evidences');
|
||||
|
||||
const claim_requestsRoutes = require('./routes/claim_requests');
|
||||
|
||||
const leadsRoutes = require('./routes/leads');
|
||||
|
||||
const lead_photosRoutes = require('./routes/lead_photos');
|
||||
@ -159,6 +161,8 @@ app.use('/api/verification_submissions', passport.authenticate('jwt', {session:
|
||||
|
||||
app.use('/api/verification_evidences', passport.authenticate('jwt', {session: false}), verification_evidencesRoutes);
|
||||
|
||||
app.use('/api/claim_requests', passport.authenticate('jwt', {session: false}), claim_requestsRoutes);
|
||||
|
||||
app.use('/api/leads', passport.authenticate('jwt', {session: false}), leadsRoutes);
|
||||
|
||||
app.use('/api/lead_photos', passport.authenticate('jwt', {session: false}), lead_photosRoutes);
|
||||
|
||||
29
backend/src/routes/claim_requests.js
Normal file
29
backend/src/routes/claim_requests.js
Normal file
@ -0,0 +1,29 @@
|
||||
|
||||
const express = require('express');
|
||||
const Claim_requestsService = require('../services/claim_requests');
|
||||
const wrapAsync = require('../helpers').wrapAsync;
|
||||
const { checkPermissions } = require('../middlewares/check-permissions');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', checkPermissions('READ_CLAIM_REQUESTS'), wrapAsync(async (req, res) => {
|
||||
const payload = await Claim_requestsService.findAll(req.query, req.currentUser);
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
router.get('/:id', checkPermissions('READ_CLAIM_REQUESTS'), wrapAsync(async (req, res) => {
|
||||
const payload = await Claim_requestsService.findBy(req.params.id);
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
router.post('/', wrapAsync(async (req, res) => {
|
||||
const payload = await Claim_requestsService.create(req.body.data, req.currentUser);
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
router.put('/:id', checkPermissions('UPDATE_CLAIM_REQUESTS'), wrapAsync(async (req, res) => {
|
||||
const payload = await Claim_requestsService.update(req.params.id, req.body.data, req.currentUser);
|
||||
res.status(200).send(payload);
|
||||
}));
|
||||
|
||||
module.exports = router;
|
||||
@ -10,13 +10,35 @@ const stream = require('stream');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
|
||||
module.exports = class BusinessesService {
|
||||
static _sanitize(data) {
|
||||
static _sanitize(data, currentUser) {
|
||||
const numericFields = ['lat', 'lng', 'reliability_score', 'response_time_median_minutes', 'rating'];
|
||||
numericFields.forEach(field => {
|
||||
if (data[field] === '') {
|
||||
data[field] = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Hide internal fields from client forms
|
||||
if (currentUser?.app_role?.name === 'Verified Business Owner') {
|
||||
const internalFields = [
|
||||
'tenant_key',
|
||||
'owner_userId',
|
||||
'owner_user',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'created_at_ts',
|
||||
'updated_at_ts',
|
||||
'reliability_score',
|
||||
'reliability_breakdown_json',
|
||||
'hours_json',
|
||||
'is_claimed',
|
||||
'is_active'
|
||||
];
|
||||
internalFields.forEach(field => {
|
||||
delete data[field];
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@ -29,9 +51,9 @@ module.exports = class BusinessesService {
|
||||
|
||||
// Ownership check for Verified Business Owner
|
||||
if (currentUser?.app_role?.name === 'Verified Business Owner') {
|
||||
if (business.owner_userId !== currentUser.id && business.id !== currentUser.businessId) {
|
||||
throw new ForbiddenError('forbidden');
|
||||
}
|
||||
// Allow viewing if owner, or if no owner (public search results might call this)
|
||||
// But the requirement says "only edit businesses where ownerUserId == currentUser.id"
|
||||
// findBy is often used for view/edit.
|
||||
}
|
||||
|
||||
return business;
|
||||
@ -40,11 +62,11 @@ module.exports = class BusinessesService {
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
data = this._sanitize(data);
|
||||
data = this._sanitize(data, currentUser);
|
||||
|
||||
// For VBOs, force the owner to be the current user
|
||||
if (currentUser?.app_role?.name === 'Verified Business Owner') {
|
||||
data.owner_user = currentUser.id;
|
||||
data.owner_userId = currentUser.id;
|
||||
data.is_active = true; // Ensure new business owner listings are active
|
||||
|
||||
// Auto-generate internal fields if missing
|
||||
@ -87,25 +109,33 @@ module.exports = class BusinessesService {
|
||||
if (!business) {
|
||||
throw new ValidationError('businessNotFound');
|
||||
}
|
||||
if (business.is_claimed) {
|
||||
if (business.owner_userId) {
|
||||
throw new ValidationError('businessAlreadyClaimed');
|
||||
}
|
||||
|
||||
await business.update({
|
||||
owner_userId: currentUser.id,
|
||||
is_claimed: true,
|
||||
}, { transaction });
|
||||
|
||||
// Link business to user if they don't have one set yet
|
||||
if (currentUser && !currentUser.businessId) {
|
||||
await db.users.update({ businessId: business.id }, {
|
||||
where: { id: currentUser.id },
|
||||
transaction
|
||||
});
|
||||
// Check for pending claim
|
||||
const pendingRequest = await db.claim_requests.findOne({
|
||||
where: {
|
||||
businessId: id,
|
||||
userId: currentUser.id,
|
||||
status: 'PENDING'
|
||||
},
|
||||
transaction
|
||||
});
|
||||
if (pendingRequest) {
|
||||
throw new ValidationError('claimRequestPending');
|
||||
}
|
||||
|
||||
// Create Claim Request
|
||||
const claim_request = await db.claim_requests.create({
|
||||
businessId: id,
|
||||
userId: currentUser.id,
|
||||
status: 'PENDING',
|
||||
createdById: currentUser.id
|
||||
}, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
return business;
|
||||
return claim_request;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
@ -150,7 +180,7 @@ module.exports = class BusinessesService {
|
||||
static async update(data, id, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
data = this._sanitize(data);
|
||||
data = this._sanitize(data, currentUser);
|
||||
|
||||
let business = await BusinessesDBApi.findBy(
|
||||
{id},
|
||||
@ -168,11 +198,13 @@ module.exports = class BusinessesService {
|
||||
if (business.owner_userId !== currentUser.id && business.id !== currentUser.businessId) {
|
||||
throw new ForbiddenError('forbidden');
|
||||
}
|
||||
// Prevent transferring ownership
|
||||
// Prevent transferring ownership or changing internal fields
|
||||
delete data.owner_user;
|
||||
delete data.owner_userId;
|
||||
delete data.slug;
|
||||
delete data.tenant_key;
|
||||
delete data.is_active;
|
||||
delete data.is_claimed;
|
||||
}
|
||||
|
||||
const updatedBusinesses = await BusinessesDBApi.update(
|
||||
|
||||
83
backend/src/services/claim_requests.js
Normal file
83
backend/src/services/claim_requests.js
Normal file
@ -0,0 +1,83 @@
|
||||
|
||||
const db = require('../db/models');
|
||||
const Claim_requestsDBApi = require('../db/api/claim_requests');
|
||||
const ValidationError = require('./notifications/errors/validation');
|
||||
const ForbiddenError = require('./notifications/errors/forbidden');
|
||||
|
||||
module.exports = class Claim_requestsService {
|
||||
static async create(data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
const business = await db.businesses.findByPk(data.businessId, { transaction });
|
||||
if (!business) throw new ValidationError('businessNotFound');
|
||||
if (business.owner_userId) throw new ValidationError('businessAlreadyOwned');
|
||||
|
||||
const existingRequest = await db.claim_requests.findOne({
|
||||
where: {
|
||||
businessId: data.businessId,
|
||||
userId: currentUser.id,
|
||||
status: 'PENDING'
|
||||
},
|
||||
transaction
|
||||
});
|
||||
if (existingRequest) throw new ValidationError('claimRequestPending');
|
||||
|
||||
const claim_request = await Claim_requestsDBApi.create(
|
||||
{
|
||||
businessId: data.businessId,
|
||||
userId: currentUser.id,
|
||||
status: 'PENDING',
|
||||
},
|
||||
{ currentUser, transaction },
|
||||
);
|
||||
|
||||
await transaction.commit();
|
||||
return claim_request;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async update(id, data, currentUser) {
|
||||
const transaction = await db.sequelize.transaction();
|
||||
try {
|
||||
const claim_request = await Claim_requestsDBApi.findBy({ id }, { transaction });
|
||||
if (!claim_request) throw new ValidationError('claimRequestNotFound');
|
||||
|
||||
// Only admin can approve/reject
|
||||
if (currentUser.app_role?.name !== 'Administrator') {
|
||||
throw new ForbiddenError('forbidden');
|
||||
}
|
||||
|
||||
const updatedRequest = await Claim_requestsDBApi.update(id, data, { currentUser, transaction });
|
||||
|
||||
// If approved, update business ownership
|
||||
if (data.status === 'APPROVED') {
|
||||
await db.businesses.update(
|
||||
{ owner_userId: claim_request.userId, is_claimed: true },
|
||||
{ where: { id: claim_request.businessId }, transaction }
|
||||
);
|
||||
// Also link to user record
|
||||
await db.users.update(
|
||||
{ businessId: claim_request.businessId },
|
||||
{ where: { id: claim_request.userId }, transaction }
|
||||
);
|
||||
}
|
||||
|
||||
await transaction.commit();
|
||||
return updatedRequest;
|
||||
} catch (error) {
|
||||
await transaction.rollback();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
static async findBy(id) {
|
||||
return await Claim_requestsDBApi.findBy({ id });
|
||||
}
|
||||
|
||||
static async findAll(query, currentUser) {
|
||||
return await Claim_requestsDBApi.findAll(query, { currentUser });
|
||||
}
|
||||
};
|
||||
@ -92,11 +92,14 @@ export default function LayoutAuthenticated({
|
||||
if (currentUser?.app_role?.name === 'Verified Business Owner') {
|
||||
const allowedPaths = [
|
||||
'/dashboard',
|
||||
'/businesses/businesses-list',
|
||||
'/my-listing',
|
||||
'/leads/leads-list',
|
||||
'/reviews/reviews-list',
|
||||
'/messages/messages-list',
|
||||
'/profile'
|
||||
'/verification_submissions/verification_submissions-list',
|
||||
'/profile',
|
||||
'/billing',
|
||||
'/team'
|
||||
];
|
||||
return allowedPaths.includes(item.href);
|
||||
}
|
||||
@ -106,9 +109,6 @@ export default function LayoutAuthenticated({
|
||||
if (item.href === '/leads/leads-list') {
|
||||
return { ...item, label: 'Service Requests' };
|
||||
}
|
||||
if (item.href === '/businesses/businesses-list') {
|
||||
return { ...item, label: 'My Listing' };
|
||||
}
|
||||
}
|
||||
return item;
|
||||
});
|
||||
@ -151,4 +151,4 @@ export default function LayoutAuthenticated({
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -39,7 +39,7 @@ const menuAside: MenuAsideItem[] = [
|
||||
roles: ['Administrator', 'Platform Owner']
|
||||
},
|
||||
|
||||
// Shared but labeled differently or scoped
|
||||
// Admin and Platform Owner see all service listings
|
||||
{
|
||||
href: '/businesses/businesses-list',
|
||||
label: 'Service Listings',
|
||||
@ -47,11 +47,21 @@ const menuAside: MenuAsideItem[] = [
|
||||
permissions: 'READ_BUSINESSES',
|
||||
roles: ['Administrator', 'Platform Owner']
|
||||
},
|
||||
|
||||
// Claim Requests (Admin Only)
|
||||
{
|
||||
href: '/businesses/businesses-list',
|
||||
label: 'My Studio',
|
||||
href: '/claim_requests/claim_requests-list',
|
||||
label: 'Claim Requests',
|
||||
icon: icon.mdiShieldCheck,
|
||||
permissions: 'READ_CLAIM_REQUESTS',
|
||||
roles: ['Administrator', 'Platform Owner']
|
||||
},
|
||||
|
||||
// Business Owner sees their My Listing page
|
||||
{
|
||||
href: '/my-listing',
|
||||
label: 'My Listing',
|
||||
icon: icon.mdiStorefront,
|
||||
permissions: 'READ_BUSINESSES',
|
||||
roles: ['Verified Business Owner']
|
||||
},
|
||||
|
||||
|
||||
@ -55,7 +55,7 @@ const BusinessesNew = () => {
|
||||
data.slug = data.name.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '');
|
||||
}
|
||||
await dispatch(create(data))
|
||||
await router.push('/businesses/businesses-list')
|
||||
await router.push(isVBO ? '/my-listing' : '/businesses/businesses-list')
|
||||
}
|
||||
|
||||
return (
|
||||
@ -133,7 +133,7 @@ const BusinessesNew = () => {
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label={isVBO ? "Create Listing" : "Submit"} />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/businesses/businesses-list')}/>
|
||||
<BaseButton type='button' color='danger' outline label='Cancel' onClick={() => router.push(isVBO ? '/my-listing' : '/businesses/businesses-list')}/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
|
||||
142
frontend/src/pages/claim_requests/claim_requests-list.tsx
Normal file
142
frontend/src/pages/claim_requests/claim_requests-list.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { mdiShieldCheck, mdiCheck, mdiClose, mdiEye } from '@mdi/js';
|
||||
import axios from 'axios';
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated';
|
||||
import SectionMain from '../../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../../components/SectionTitleLineWithButton';
|
||||
import CardBox from '../../components/CardBox';
|
||||
import BaseButton from '../../components/BaseButton';
|
||||
import BaseIcon from '../../components/BaseIcon';
|
||||
import LoadingSpinner from '../../components/LoadingSpinner';
|
||||
import { getPageTitle } from '../../config';
|
||||
|
||||
const ClaimRequestsListPage = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [requests, setRequests] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchRequests();
|
||||
}, []);
|
||||
|
||||
const fetchRequests = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await axios.get('/claim_requests');
|
||||
setRequests(response.data.rows);
|
||||
} catch (error) {
|
||||
console.error('Error fetching claim requests:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleAction = async (id: string, status: string) => {
|
||||
let rejectionReason = '';
|
||||
if (status === 'REJECTED') {
|
||||
rejectionReason = prompt('Please enter rejection reason:') || 'Documentation insufficient';
|
||||
}
|
||||
try {
|
||||
await axios.put(`/claim_requests/${id}`, { data: { status, rejectionReason } });
|
||||
fetchRequests();
|
||||
} catch (error) {
|
||||
console.error('Error updating claim request:', error);
|
||||
alert('Failed to update request.');
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <SectionMain><LoadingSpinner /></SectionMain>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('Claim Requests')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiShieldCheck} title="Claim Requests" main />
|
||||
|
||||
<CardBox className="overflow-x-auto">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-100">
|
||||
<th className="p-4 font-bold text-slate-500 uppercase text-xs">Business</th>
|
||||
<th className="p-4 font-bold text-slate-500 uppercase text-xs">User</th>
|
||||
<th className="p-4 font-bold text-slate-500 uppercase text-xs">Status</th>
|
||||
<th className="p-4 font-bold text-slate-500 uppercase text-xs">Date</th>
|
||||
<th className="p-4 font-bold text-slate-500 uppercase text-xs text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{requests.map((request) => (
|
||||
<tr key={request.id} className="border-b border-slate-50 hover:bg-slate-50 transition-colors">
|
||||
<td className="p-4">
|
||||
<div className="font-bold">{request.business?.name}</div>
|
||||
<div className="text-xs text-slate-400">{request.business?.city}, {request.business?.state}</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<div className="font-medium">{request.user?.firstName} {request.user?.lastName}</div>
|
||||
<div className="text-xs text-slate-400">{request.user?.email}</div>
|
||||
</td>
|
||||
<td className="p-4">
|
||||
<span className={`px-2 py-1 rounded-full text-[10px] font-bold uppercase ${
|
||||
request.status === 'APPROVED' ? 'bg-emerald-50 text-emerald-600' :
|
||||
request.status === 'REJECTED' ? 'bg-rose-50 text-rose-600' :
|
||||
'bg-amber-50 text-amber-600'
|
||||
}`}>
|
||||
{request.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="p-4 text-sm text-slate-500">
|
||||
{new Date(request.createdAt).toLocaleDateString()}
|
||||
</td>
|
||||
<td className="p-4 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<BaseButton
|
||||
icon={mdiEye}
|
||||
color="info"
|
||||
small
|
||||
href={`/public/businesses-details?id=${request.businessId}`}
|
||||
target="_blank"
|
||||
/>
|
||||
{request.status === 'PENDING' && (
|
||||
<>
|
||||
<BaseButton
|
||||
icon={mdiCheck}
|
||||
color="success"
|
||||
small
|
||||
onClick={() => handleAction(request.id, 'APPROVED')}
|
||||
/>
|
||||
<BaseButton
|
||||
icon={mdiClose}
|
||||
color="danger"
|
||||
small
|
||||
onClick={() => handleAction(request.id, 'REJECTED')}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{requests.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={5} className="p-10 text-center text-slate-400 italic">No claim requests found.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</CardBox>
|
||||
</SectionMain>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
ClaimRequestsListPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return (
|
||||
<LayoutAuthenticated permission="READ_CLAIM_REQUESTS">
|
||||
{page}
|
||||
</LayoutAuthenticated>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClaimRequestsListPage;
|
||||
234
frontend/src/pages/my-listing.tsx
Normal file
234
frontend/src/pages/my-listing.tsx
Normal file
@ -0,0 +1,234 @@
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import {
|
||||
mdiStorefront,
|
||||
mdiShieldCheck,
|
||||
mdiAlertCircle,
|
||||
mdiCheckCircle,
|
||||
mdiPlus,
|
||||
mdiMagnify,
|
||||
mdiPencil
|
||||
} from '@mdi/js';
|
||||
import axios from 'axios';
|
||||
import LayoutAuthenticated from '../layouts/Authenticated';
|
||||
import SectionMain from '../components/SectionMain';
|
||||
import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
|
||||
import CardBox from '../components/CardBox';
|
||||
import BaseButton from '../components/BaseButton';
|
||||
import BaseIcon from '../components/BaseIcon';
|
||||
import LoadingSpinner from '../components/LoadingSpinner';
|
||||
import { useAppSelector } from '../stores/hooks';
|
||||
import { getPageTitle } from '../config';
|
||||
|
||||
const MyListingPage = () => {
|
||||
const router = useRouter();
|
||||
const { currentUser } = useAppSelector((state) => state.auth);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [myBusiness, setMyBusiness] = useState<any>(null);
|
||||
const [pendingClaim, setPendingClaim] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser) {
|
||||
fetchData();
|
||||
}
|
||||
}, [currentUser]);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 1. Fetch owned business
|
||||
let business = null;
|
||||
if (currentUser.businessId) {
|
||||
const res = await axios.get(`/businesses/${currentUser.businessId}`);
|
||||
business = res.data;
|
||||
} else {
|
||||
// Search by owner_userId if businessId is not set on user record yet
|
||||
const res = await axios.get('/businesses', { params: { owner_userId: currentUser.id } });
|
||||
if (res.data.rows && res.data.rows.length > 0) {
|
||||
business = res.data.rows[0];
|
||||
}
|
||||
}
|
||||
setMyBusiness(business);
|
||||
|
||||
// 2. If no business, fetch pending claim
|
||||
if (!business) {
|
||||
const res = await axios.get('/claim_requests', { params: { userId: currentUser.id, status: 'PENDING' } });
|
||||
if (res.data.rows && res.data.rows.length > 0) {
|
||||
setPendingClaim(res.data.rows[0]);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <SectionMain><LoadingSpinner /></SectionMain>;
|
||||
|
||||
// STATE 1: Owns a business
|
||||
if (myBusiness) {
|
||||
return (
|
||||
<SectionMain>
|
||||
<Head>
|
||||
<title>{getPageTitle('My Listing')}</title>
|
||||
</Head>
|
||||
<SectionTitleLineWithButton icon={mdiStorefront} title="My Listing" main>
|
||||
<BaseButton
|
||||
href={`/businesses/businesses-edit/?id=${myBusiness.id}`}
|
||||
icon={mdiPencil}
|
||||
label="Edit Listing"
|
||||
color="info"
|
||||
/>
|
||||
</SectionTitleLineWithButton>
|
||||
|
||||
<CardBox className="mb-6">
|
||||
<div className="flex flex-col md:flex-row items-center gap-8 p-4">
|
||||
<div className="w-32 h-32 bg-slate-100 rounded-3xl flex items-center justify-center text-slate-400">
|
||||
<BaseIcon path={mdiStorefront} size={48} />
|
||||
</div>
|
||||
<div className="flex-grow text-center md:text-left">
|
||||
<h2 className="text-3xl font-bold mb-2">{myBusiness.name}</h2>
|
||||
<p className="text-slate-500 mb-4">{myBusiness.address}, {myBusiness.city}, {myBusiness.state}</p>
|
||||
<div className="flex flex-wrap justify-center md:justify-start gap-4">
|
||||
<span className="bg-emerald-50 text-emerald-600 px-3 py-1 rounded-full text-xs font-bold uppercase">Active Listing</span>
|
||||
<span className="bg-blue-50 text-blue-600 px-3 py-1 rounded-full text-xs font-bold uppercase">Verified Owner</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<BaseButton
|
||||
href={`/public/businesses-details?id=${myBusiness.id}`}
|
||||
label="View Public Profile"
|
||||
outline
|
||||
color="info"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardBox>
|
||||
|
||||
{/* Placeholder for stats or recent bookings */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<CardBox className="p-6 text-center">
|
||||
<div className="text-slate-400 text-xs font-bold uppercase tracking-widest mb-2">Total Love Letters</div>
|
||||
<div className="text-3xl font-bold">{myBusiness.reviews_business?.length || 0}</div>
|
||||
</CardBox>
|
||||
<CardBox className="p-6 text-center">
|
||||
<div className="text-slate-400 text-xs font-bold uppercase tracking-widest mb-2">Avg Rating</div>
|
||||
<div className="text-3xl font-bold">{myBusiness.rating || 'New'}</div>
|
||||
</CardBox>
|
||||
<CardBox className="p-6 text-center">
|
||||
<div className="text-slate-400 text-xs font-bold uppercase tracking-widest mb-2">Service Bookings</div>
|
||||
<div className="text-3xl font-bold">0</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
</SectionMain>
|
||||
);
|
||||
}
|
||||
|
||||
// STATE 2: Pending claim
|
||||
if (pendingClaim) {
|
||||
return (
|
||||
<SectionMain>
|
||||
<Head>
|
||||
<title>{getPageTitle('Claim Pending')}</title>
|
||||
</Head>
|
||||
<SectionTitleLineWithButton icon={mdiShieldCheck} title="Claim Verification" main />
|
||||
|
||||
<div className="max-w-3xl mx-auto">
|
||||
<CardBox className="p-10 text-center space-y-6">
|
||||
<div className="w-20 h-20 bg-amber-100 text-amber-600 rounded-full flex items-center justify-center mx-auto">
|
||||
<BaseIcon path={mdiAlertCircle} size={48} />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold mb-2">Claim Request Pending</h2>
|
||||
<p className="text-slate-500 max-w-md mx-auto">
|
||||
We've received your request to claim <strong>{pendingClaim.business?.name}</strong>.
|
||||
Our team is currently reviewing your application.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-200 inline-block text-left w-full max-w-md">
|
||||
<h4 className="font-bold mb-4 flex items-center">
|
||||
<BaseIcon path={mdiShieldCheck} size={20} className="mr-2 text-emerald-500" />
|
||||
Next Steps: Verification
|
||||
</h4>
|
||||
<p className="text-sm text-slate-600 mb-6">
|
||||
To approve your claim, we need to verify your association with this business. Please upload a business license or utility bill.
|
||||
</p>
|
||||
<BaseButton
|
||||
href={`/verification_submissions/verification_submissions-new?businessId=${pendingClaim.businessId}`}
|
||||
label="Upload Verification Documents"
|
||||
color="info"
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-slate-100">
|
||||
<p className="text-xs text-slate-400">Request ID: {pendingClaim.id} • Submitted on {new Date(pendingClaim.createdAt).toLocaleDateString()}</p>
|
||||
</div>
|
||||
</CardBox>
|
||||
</div>
|
||||
</SectionMain>
|
||||
);
|
||||
}
|
||||
|
||||
// STATE 3: Neither
|
||||
return (
|
||||
<SectionMain>
|
||||
<Head>
|
||||
<title>{getPageTitle('My Listing')}</title>
|
||||
</Head>
|
||||
<SectionTitleLineWithButton icon={mdiStorefront} title="My Listing" main />
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 max-w-5xl mx-auto pt-10">
|
||||
|
||||
{/* Path 1: Create New */}
|
||||
<CardBox className="p-10 flex flex-col items-center text-center space-y-6 hover:shadow-xl transition-all border-2 border-transparent hover:border-emerald-100">
|
||||
<div className="w-16 h-16 bg-emerald-100 text-emerald-600 rounded-2xl flex items-center justify-center">
|
||||
<BaseIcon path={mdiPlus} size={40} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-2">Create New Listing</h3>
|
||||
<p className="text-slate-500 text-sm">
|
||||
Your business isn't on Fix-It-Local yet? Create a fresh listing and start attracting clients immediately.
|
||||
</p>
|
||||
</div>
|
||||
<BaseButton
|
||||
href="/businesses/businesses-new"
|
||||
label="Start Fresh Listing"
|
||||
color="info"
|
||||
className="w-full"
|
||||
/>
|
||||
</CardBox>
|
||||
|
||||
{/* Path 2: Claim Existing */}
|
||||
<CardBox className="p-10 flex flex-col items-center text-center space-y-6 hover:shadow-xl transition-all border-2 border-transparent hover:border-blue-100">
|
||||
<div className="w-16 h-16 bg-blue-100 text-blue-600 rounded-2xl flex items-center justify-center">
|
||||
<BaseIcon path={mdiMagnify} size={40} />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-bold mb-2">Claim Existing Business</h3>
|
||||
<p className="text-slate-500 text-sm">
|
||||
Search for your business in our directory. If it exists but is unowned, you can claim it for free.
|
||||
</p>
|
||||
</div>
|
||||
<BaseButton
|
||||
href="/search"
|
||||
label="Search Directory"
|
||||
outline
|
||||
color="info"
|
||||
className="w-full"
|
||||
/>
|
||||
</CardBox>
|
||||
|
||||
</div>
|
||||
</SectionMain>
|
||||
);
|
||||
};
|
||||
|
||||
MyListingPage.getLayout = function getLayout(page: ReactElement) {
|
||||
return <LayoutAuthenticated>{page}</LayoutAuthenticated>;
|
||||
};
|
||||
|
||||
export default MyListingPage;
|
||||
@ -63,16 +63,17 @@ const BusinessDetailsPublic = () => {
|
||||
|
||||
const claimListing = async () => {
|
||||
if (!currentUser) {
|
||||
router.push('/login');
|
||||
// Redirect to login with original destination
|
||||
router.push(`/login?redirect=${encodeURIComponent(router.asPath)}`);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await axios.post(`/businesses/${id}/claim`);
|
||||
// After claiming, redirect to dashboard as requested by user
|
||||
router.push('/dashboard');
|
||||
// After claiming, redirect to my-listing as requested
|
||||
router.push('/my-listing');
|
||||
} catch (error) {
|
||||
console.error('Error claiming business:', error);
|
||||
alert('Failed to claim business. Please try again.');
|
||||
alert('Failed to claim business. It might already be claimed or you have a pending request.');
|
||||
}
|
||||
};
|
||||
|
||||
@ -123,7 +124,7 @@ const BusinessDetailsPublic = () => {
|
||||
) : (
|
||||
<BaseIcon path={mdiShieldCheck} size={64} className="text-slate-300" />
|
||||
)}
|
||||
{(business.reliability_score >= 80 || business.is_claimed) && (
|
||||
{(business.reliability_score >= 80 || business.owner_userId) && (
|
||||
<div className="absolute -top-2 -right-2 bg-emerald-500 text-white p-2 rounded-full shadow-lg">
|
||||
<BaseIcon path={mdiCheckDecagram} size={24} />
|
||||
</div>
|
||||
@ -143,7 +144,7 @@ const BusinessDetailsPublic = () => {
|
||||
<BaseIcon path={mdiStar} size={18} className="mr-1 text-amber-400" />
|
||||
{displayRating} Rating
|
||||
</span>
|
||||
{business.is_claimed ? (
|
||||
{business.owner_userId ? (
|
||||
<span className="bg-emerald-50 text-emerald-600 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wider">
|
||||
Verified Pro
|
||||
</span>
|
||||
@ -196,7 +197,7 @@ const BusinessDetailsPublic = () => {
|
||||
{/* Main Content */}
|
||||
<div className="lg:col-span-2 space-y-12">
|
||||
|
||||
{!business.is_claimed && (
|
||||
{!business.owner_userId && (
|
||||
<div className="bg-amber-50 border border-amber-200 p-8 rounded-[2rem] flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div>
|
||||
<h4 className="text-xl font-bold text-amber-900 mb-2">Is this your business?</h4>
|
||||
@ -401,7 +402,7 @@ const BusinessDetailsPublic = () => {
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{business.is_claimed && (
|
||||
{business.owner_userId && (
|
||||
<div className="flex items-center p-4 rounded-2xl bg-emerald-50">
|
||||
<div className="w-10 h-10 bg-emerald-200 rounded-xl flex items-center justify-center mr-4 text-emerald-700">
|
||||
<BaseIcon path={mdiCheckDecagram} size={24} />
|
||||
@ -412,7 +413,7 @@ const BusinessDetailsPublic = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!business.business_badges_business?.length && !business.is_claimed && <p className="text-slate-400 text-sm italic">Pending verification...</p>}
|
||||
{!business.business_badges_business?.length && !business.owner_userId && <p className="text-slate-400 text-sm italic">Pending verification...</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { mdiAccount, mdiChartTimelineVariant, mdiMail, mdiUpload } from '@mdi/js'
|
||||
import Head from 'next/head'
|
||||
import React, { ReactElement } from 'react'
|
||||
import React, { ReactElement, useEffect, useState } from 'react'
|
||||
import CardBox from '../../components/CardBox'
|
||||
import LayoutAuthenticated from '../../layouts/Authenticated'
|
||||
import SectionMain from '../../components/SectionMain'
|
||||
@ -27,206 +27,66 @@ import { useAppDispatch } from '../../stores/hooks'
|
||||
import { useRouter } from 'next/router'
|
||||
import moment from 'moment';
|
||||
|
||||
const initialValues = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const initialValuesDefault = {
|
||||
business: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
badge_type: 'VERIFIED_BUSINESS',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
status: 'PENDING',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
notes: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
admin_notes: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
created_at_ts: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
updated_at_ts: '',
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
const Verification_submissionsNew = () => {
|
||||
const router = useRouter()
|
||||
const dispatch = useAppDispatch()
|
||||
const { businessId } = router.query
|
||||
const [initialValues, setInitialValues] = useState(initialValuesDefault)
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (businessId) {
|
||||
setInitialValues({
|
||||
...initialValuesDefault,
|
||||
business: businessId as string,
|
||||
badge_type: 'VERIFIED_BUSINESS'
|
||||
})
|
||||
}
|
||||
}, [businessId])
|
||||
|
||||
const handleSubmit = async (data) => {
|
||||
await dispatch(create(data))
|
||||
await router.push('/verification_submissions/verification_submissions-list')
|
||||
if (businessId) {
|
||||
router.push('/my-listing')
|
||||
} else {
|
||||
await router.push('/verification_submissions/verification_submissions-list')
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{getPageTitle('New Item')}</title>
|
||||
<title>{getPageTitle('Verification Submission')}</title>
|
||||
</Head>
|
||||
<SectionMain>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="New Item" main>
|
||||
<SectionTitleLineWithButton icon={mdiChartTimelineVariant} title="Verification Submission" main>
|
||||
{''}
|
||||
</SectionTitleLineWithButton>
|
||||
<CardBox>
|
||||
<Formik
|
||||
initialValues={
|
||||
|
||||
initialValues
|
||||
|
||||
}
|
||||
enableReinitialize
|
||||
initialValues={initialValues}
|
||||
onSubmit={(values) => handleSubmit(values)}
|
||||
>
|
||||
<Form>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Business" labelFor="business">
|
||||
<Field name="business" id="business" component={SelectField} options={[]} itemRef={'businesses'}></Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="BadgeType" labelFor="badge_type">
|
||||
<FormField label="Badge Type" labelFor="badge_type">
|
||||
<Field name="badge_type" id="badge_type" component="select">
|
||||
|
||||
<option value="VERIFIED_BUSINESS">VERIFIED_BUSINESS</option>
|
||||
<option value="VERIFIED_BUSINESS">VERIFIED_BUSINESS (Ownership Claim)</option>
|
||||
|
||||
<option value="VERIFIED_LICENSE">VERIFIED_LICENSE</option>
|
||||
|
||||
@ -241,32 +101,6 @@ const Verification_submissionsNew = () => {
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Status" labelFor="status">
|
||||
<Field name="status" id="status" component="select">
|
||||
|
||||
@ -279,90 +113,16 @@ const Verification_submissionsNew = () => {
|
||||
</Field>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="Notes" hasTextareaHeight>
|
||||
<Field name="notes" as="textarea" placeholder="Notes" />
|
||||
<FormField label="Notes / Verification Proof Description" hasTextareaHeight>
|
||||
<Field name="notes" as="textarea" placeholder="Enter details about your verification proof..." />
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField label="AdminNotes" hasTextareaHeight>
|
||||
<Field name="admin_notes" as="textarea" placeholder="AdminNotes" />
|
||||
<FormField label="Admin Notes" hasTextareaHeight>
|
||||
<Field name="admin_notes" as="textarea" placeholder="Admin notes..." />
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="CreatedAt"
|
||||
label="Created At"
|
||||
>
|
||||
<Field
|
||||
type="datetime-local"
|
||||
@ -371,61 +131,11 @@ const Verification_submissionsNew = () => {
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<FormField
|
||||
label="UpdatedAt"
|
||||
>
|
||||
<Field
|
||||
type="datetime-local"
|
||||
name="updated_at_ts"
|
||||
placeholder="UpdatedAt"
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<BaseDivider />
|
||||
<BaseButtons>
|
||||
<BaseButton type="submit" color="info" label="Submit" />
|
||||
<BaseButton type="submit" color="info" label="Submit Verification" />
|
||||
<BaseButton type="reset" color="info" outline label="Reset" />
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.push('/verification_submissions/verification_submissions-list')}/>
|
||||
<BaseButton type='reset' color='danger' outline label='Cancel' onClick={() => router.back()}/>
|
||||
</BaseButtons>
|
||||
</Form>
|
||||
</Formik>
|
||||
@ -447,4 +157,4 @@ Verification_submissionsNew.getLayout = function getLayout(page: ReactElement) {
|
||||
)
|
||||
}
|
||||
|
||||
export default Verification_submissionsNew
|
||||
export default Verification_submissionsNew
|
||||
Loading…
x
Reference in New Issue
Block a user