optimized condition checks
This commit is contained in:
@@ -7,6 +7,49 @@ const { IMAGE_LIMITS } = require("../config/imageLimits");
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
// Get condition checks for multiple rentals in a single request (batch)
|
||||||
|
router.get("/batch", authenticateToken, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { rentalIds } = req.query;
|
||||||
|
|
||||||
|
if (!rentalIds) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
conditionChecks: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const ids = rentalIds.split(",").filter((id) => id.trim());
|
||||||
|
|
||||||
|
if (ids.length === 0) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
conditionChecks: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const conditionChecks =
|
||||||
|
await ConditionCheckService.getConditionChecksForRentals(ids);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
conditionChecks,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const reqLogger = logger.withRequestId(req.id);
|
||||||
|
reqLogger.error("Error fetching batch condition checks", {
|
||||||
|
error: error.message,
|
||||||
|
stack: error.stack,
|
||||||
|
rentalIds: req.query.rentalIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: "Failed to fetch condition checks",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Submit a condition check
|
// Submit a condition check
|
||||||
router.post("/:rentalId", authenticateToken, async (req, res) => {
|
router.post("/:rentalId", authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -20,9 +63,13 @@ router.post("/:rentalId", authenticateToken, async (req, res) => {
|
|||||||
: [];
|
: [];
|
||||||
|
|
||||||
// Validate S3 keys format and folder
|
// Validate S3 keys format and folder
|
||||||
const keyValidation = validateS3Keys(imageFilenamesArray, "condition-checks", {
|
const keyValidation = validateS3Keys(
|
||||||
maxKeys: IMAGE_LIMITS.conditionChecks,
|
imageFilenamesArray,
|
||||||
});
|
"condition-checks",
|
||||||
|
{
|
||||||
|
maxKeys: IMAGE_LIMITS.conditionChecks,
|
||||||
|
}
|
||||||
|
);
|
||||||
if (!keyValidation.valid) {
|
if (!keyValidation.valid) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -69,69 +116,16 @@ router.post("/:rentalId", authenticateToken, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get condition checks for a rental
|
|
||||||
router.get("/:rentalId", authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { rentalId } = req.params;
|
|
||||||
|
|
||||||
const conditionChecks = await ConditionCheckService.getConditionChecks(
|
|
||||||
rentalId
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
conditionChecks,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
|
||||||
reqLogger.error("Error fetching condition checks", {
|
|
||||||
error: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
rentalId: req.params.rentalId,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: "Failed to fetch condition checks",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get condition check timeline for a rental
|
|
||||||
router.get("/:rentalId/timeline", authenticateToken, async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { rentalId } = req.params;
|
|
||||||
|
|
||||||
const timeline = await ConditionCheckService.getConditionCheckTimeline(
|
|
||||||
rentalId
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
timeline,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
const reqLogger = logger.withRequestId(req.id);
|
|
||||||
reqLogger.error("Error fetching condition check timeline", {
|
|
||||||
error: error.message,
|
|
||||||
stack: error.stack,
|
|
||||||
rentalId: req.params.rentalId,
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get available condition checks for current user
|
// Get available condition checks for current user
|
||||||
router.get("/", authenticateToken, async (req, res) => {
|
router.get("/", authenticateToken, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.user.id;
|
const userId = req.user.id;
|
||||||
|
const { rentalIds } = req.query;
|
||||||
|
const ids = rentalIds ? rentalIds.split(",").filter((id) => id.trim()) : [];
|
||||||
|
|
||||||
const availableChecks = await ConditionCheckService.getAvailableChecks(
|
const availableChecks = await ConditionCheckService.getAvailableChecks(
|
||||||
userId
|
userId,
|
||||||
|
ids
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
|
|||||||
@@ -156,13 +156,21 @@ class ConditionCheckService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all condition checks for a rental
|
* Get all condition checks for multiple rentals (batch)
|
||||||
* @param {string} rentalId - Rental ID
|
* @param {Array<string>} rentalIds - Array of Rental IDs
|
||||||
* @returns {Array} - Array of condition checks with user info
|
* @returns {Array} - Array of condition checks with user info
|
||||||
*/
|
*/
|
||||||
static async getConditionChecks(rentalId) {
|
static async getConditionChecksForRentals(rentalIds) {
|
||||||
|
if (!rentalIds || rentalIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const checks = await ConditionCheck.findAll({
|
const checks = await ConditionCheck.findAll({
|
||||||
where: { rentalId },
|
where: {
|
||||||
|
rentalId: {
|
||||||
|
[Op.in]: rentalIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
include: [
|
include: [
|
||||||
{
|
{
|
||||||
model: User,
|
model: User,
|
||||||
@@ -176,119 +184,24 @@ class ConditionCheckService {
|
|||||||
return checks;
|
return checks;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get condition check timeline for a rental
|
|
||||||
* @param {string} rentalId - Rental ID
|
|
||||||
* @returns {Object} - Timeline showing what checks are available/completed
|
|
||||||
*/
|
|
||||||
static async getConditionCheckTimeline(rentalId) {
|
|
||||||
const rental = await Rental.findByPk(rentalId);
|
|
||||||
if (!rental) {
|
|
||||||
throw new Error("Rental not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingChecks = await ConditionCheck.findAll({
|
|
||||||
where: { rentalId },
|
|
||||||
include: [
|
|
||||||
{
|
|
||||||
model: User,
|
|
||||||
as: "submittedByUser",
|
|
||||||
attributes: ["id", "firstName", "lastName"],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkTypes = [
|
|
||||||
"pre_rental_owner",
|
|
||||||
"rental_start_renter",
|
|
||||||
"rental_end_renter",
|
|
||||||
"post_rental_owner",
|
|
||||||
];
|
|
||||||
|
|
||||||
const timeline = {};
|
|
||||||
|
|
||||||
for (const checkType of checkTypes) {
|
|
||||||
const existingCheck = existingChecks.find(
|
|
||||||
(check) => check.checkType === checkType
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingCheck) {
|
|
||||||
timeline[checkType] = {
|
|
||||||
status: "completed",
|
|
||||||
submittedAt: existingCheck.submittedAt,
|
|
||||||
submittedBy: existingCheck.submittedBy,
|
|
||||||
photoCount: existingCheck.imageFilenames.length,
|
|
||||||
hasNotes: !!existingCheck.notes,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Calculate if this check type is available
|
|
||||||
const now = new Date();
|
|
||||||
const startDate = new Date(rental.startDateTime);
|
|
||||||
const endDate = new Date(rental.endDateTime);
|
|
||||||
const twentyFourHours = 24 * 60 * 60 * 1000;
|
|
||||||
|
|
||||||
let timeWindow = {};
|
|
||||||
let status = "not_available";
|
|
||||||
|
|
||||||
switch (checkType) {
|
|
||||||
case "pre_rental_owner":
|
|
||||||
timeWindow.start = new Date(startDate.getTime() - twentyFourHours);
|
|
||||||
timeWindow.end = startDate;
|
|
||||||
break;
|
|
||||||
case "rental_start_renter":
|
|
||||||
timeWindow.start = startDate;
|
|
||||||
timeWindow.end = new Date(startDate.getTime() + twentyFourHours);
|
|
||||||
break;
|
|
||||||
case "rental_end_renter":
|
|
||||||
timeWindow.start = new Date(endDate.getTime() - twentyFourHours);
|
|
||||||
timeWindow.end = endDate;
|
|
||||||
break;
|
|
||||||
case "post_rental_owner":
|
|
||||||
timeWindow.start = endDate;
|
|
||||||
timeWindow.end = new Date(endDate.getTime() + twentyFourHours);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (now >= timeWindow.start && now <= timeWindow.end) {
|
|
||||||
status = "available";
|
|
||||||
} else if (now < timeWindow.start) {
|
|
||||||
status = "pending";
|
|
||||||
} else {
|
|
||||||
status = "expired";
|
|
||||||
}
|
|
||||||
|
|
||||||
timeline[checkType] = {
|
|
||||||
status,
|
|
||||||
timeWindow,
|
|
||||||
availableFrom: timeWindow.start,
|
|
||||||
availableUntil: timeWindow.end,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
rental: {
|
|
||||||
id: rental.id,
|
|
||||||
startDateTime: rental.startDateTime,
|
|
||||||
endDateTime: rental.endDateTime,
|
|
||||||
status: rental.status,
|
|
||||||
},
|
|
||||||
timeline,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get available condition checks for a user
|
* Get available condition checks for a user
|
||||||
* @param {string} userId - User ID
|
* @param {string} userId - User ID
|
||||||
|
* @param {Array<string>} rentalIds - Array of rental IDs to check
|
||||||
* @returns {Array} - Array of available condition checks
|
* @returns {Array} - Array of available condition checks
|
||||||
*/
|
*/
|
||||||
static async getAvailableChecks(userId) {
|
static async getAvailableChecks(userId, rentalIds) {
|
||||||
|
if (!rentalIds || rentalIds.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const twentyFourHours = 24 * 60 * 60 * 1000;
|
const twentyFourHours = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
// Find rentals where user is owner or renter
|
// Find specified rentals where user is owner or renter
|
||||||
const rentals = await Rental.findAll({
|
const rentals = await Rental.findAll({
|
||||||
where: {
|
where: {
|
||||||
|
id: { [Op.in]: rentalIds },
|
||||||
[Op.or]: [{ ownerId: userId }, { renterId: userId }],
|
[Op.or]: [{ ownerId: userId }, { renterId: userId }],
|
||||||
status: {
|
status: {
|
||||||
[Op.in]: ["confirmed", "active", "completed"],
|
[Op.in]: ["confirmed", "active", "completed"],
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ jest.mock('../../../middleware/auth', () => ({
|
|||||||
|
|
||||||
jest.mock('../../../services/conditionCheckService', () => ({
|
jest.mock('../../../services/conditionCheckService', () => ({
|
||||||
submitConditionCheck: jest.fn(),
|
submitConditionCheck: jest.fn(),
|
||||||
getConditionChecks: jest.fn(),
|
|
||||||
getConditionCheckTimeline: jest.fn(),
|
|
||||||
getAvailableChecks: jest.fn(),
|
getAvailableChecks: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -183,97 +181,8 @@ describe('Condition Check Routes', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /condition-checks/:rentalId', () => {
|
|
||||||
it('should return condition checks for a rental', async () => {
|
|
||||||
const mockChecks = [
|
|
||||||
{
|
|
||||||
id: 'check-1',
|
|
||||||
checkType: 'pre_rental',
|
|
||||||
notes: 'Good condition',
|
|
||||||
createdAt: '2024-01-01T00:00:00Z',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'check-2',
|
|
||||||
checkType: 'post_rental',
|
|
||||||
notes: 'Minor wear',
|
|
||||||
createdAt: '2024-01-15T00:00:00Z',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
ConditionCheckService.getConditionChecks.mockResolvedValue(mockChecks);
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.get('/condition-checks/rental-123');
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body.success).toBe(true);
|
|
||||||
expect(response.body.conditionChecks).toHaveLength(2);
|
|
||||||
expect(response.body.conditionChecks[0].checkType).toBe('pre_rental');
|
|
||||||
expect(ConditionCheckService.getConditionChecks).toHaveBeenCalledWith('rental-123');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return empty array when no checks exist', async () => {
|
|
||||||
ConditionCheckService.getConditionChecks.mockResolvedValue([]);
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.get('/condition-checks/rental-456');
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body.success).toBe(true);
|
|
||||||
expect(response.body.conditionChecks).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle service errors', async () => {
|
|
||||||
ConditionCheckService.getConditionChecks.mockRejectedValue(
|
|
||||||
new Error('Database error')
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.get('/condition-checks/rental-123');
|
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
|
||||||
expect(response.body.success).toBe(false);
|
|
||||||
expect(response.body.error).toBe('Failed to fetch condition checks');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /condition-checks/:rentalId/timeline', () => {
|
|
||||||
it('should return condition check timeline', async () => {
|
|
||||||
const mockTimeline = {
|
|
||||||
rental: { id: 'rental-123', status: 'completed' },
|
|
||||||
checks: [
|
|
||||||
{ type: 'pre_rental', status: 'completed', completedAt: '2024-01-01' },
|
|
||||||
{ type: 'post_rental', status: 'pending', completedAt: null },
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
ConditionCheckService.getConditionCheckTimeline.mockResolvedValue(mockTimeline);
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.get('/condition-checks/rental-123/timeline');
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body.success).toBe(true);
|
|
||||||
expect(response.body.timeline).toMatchObject(mockTimeline);
|
|
||||||
expect(ConditionCheckService.getConditionCheckTimeline).toHaveBeenCalledWith('rental-123');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle service errors', async () => {
|
|
||||||
ConditionCheckService.getConditionCheckTimeline.mockRejectedValue(
|
|
||||||
new Error('Rental not found')
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await request(app)
|
|
||||||
.get('/condition-checks/rental-123/timeline');
|
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
|
||||||
expect(response.body.success).toBe(false);
|
|
||||||
expect(response.body.error).toBe('Rental not found');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /condition-checks', () => {
|
describe('GET /condition-checks', () => {
|
||||||
it('should return available checks for current user', async () => {
|
it('should return available checks for specified rentals', async () => {
|
||||||
const mockAvailableChecks = [
|
const mockAvailableChecks = [
|
||||||
{
|
{
|
||||||
rentalId: 'rental-1',
|
rentalId: 'rental-1',
|
||||||
@@ -292,16 +201,16 @@ describe('Condition Check Routes', () => {
|
|||||||
ConditionCheckService.getAvailableChecks.mockResolvedValue(mockAvailableChecks);
|
ConditionCheckService.getAvailableChecks.mockResolvedValue(mockAvailableChecks);
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.get('/condition-checks');
|
.get('/condition-checks?rentalIds=rental-1,rental-2');
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
expect(response.body.availableChecks).toHaveLength(2);
|
expect(response.body.availableChecks).toHaveLength(2);
|
||||||
expect(response.body.availableChecks[0].itemName).toBe('Camera');
|
expect(response.body.availableChecks[0].itemName).toBe('Camera');
|
||||||
expect(ConditionCheckService.getAvailableChecks).toHaveBeenCalledWith('user-123');
|
expect(ConditionCheckService.getAvailableChecks).toHaveBeenCalledWith('user-123', ['rental-1', 'rental-2']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return empty array when no checks available', async () => {
|
it('should return empty array when no rentalIds provided', async () => {
|
||||||
ConditionCheckService.getAvailableChecks.mockResolvedValue([]);
|
ConditionCheckService.getAvailableChecks.mockResolvedValue([]);
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
@@ -310,6 +219,7 @@ describe('Condition Check Routes', () => {
|
|||||||
expect(response.status).toBe(200);
|
expect(response.status).toBe(200);
|
||||||
expect(response.body.success).toBe(true);
|
expect(response.body.success).toBe(true);
|
||||||
expect(response.body.availableChecks).toHaveLength(0);
|
expect(response.body.availableChecks).toHaveLength(0);
|
||||||
|
expect(ConditionCheckService.getAvailableChecks).toHaveBeenCalledWith('user-123', []);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle service errors', async () => {
|
it('should handle service errors', async () => {
|
||||||
@@ -318,7 +228,7 @@ describe('Condition Check Routes', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.get('/condition-checks');
|
.get('/condition-checks?rentalIds=rental-1');
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
expect(response.status).toBe(500);
|
||||||
expect(response.body.success).toBe(false);
|
expect(response.body.success).toBe(false);
|
||||||
|
|||||||
@@ -122,44 +122,4 @@ describe('ConditionCheckService', () => {
|
|||||||
).rejects.toThrow('Rental not found');
|
).rejects.toThrow('Rental not found');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getConditionChecks', () => {
|
|
||||||
it('should retrieve condition checks for rental', async () => {
|
|
||||||
const mockChecks = [
|
|
||||||
{
|
|
||||||
id: 'check-1',
|
|
||||||
checkType: 'pre_rental_owner',
|
|
||||||
submittedBy: 'owner-456',
|
|
||||||
submittedAt: '2023-05-31T12:00:00Z',
|
|
||||||
photos: ['/uploads/photo1.jpg'],
|
|
||||||
notes: 'Item ready'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'check-2',
|
|
||||||
checkType: 'rental_start_renter',
|
|
||||||
submittedBy: 'renter-789',
|
|
||||||
submittedAt: '2023-06-01T11:00:00Z',
|
|
||||||
photos: ['/uploads/photo2.jpg'],
|
|
||||||
notes: 'Item received'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
ConditionCheck.findAll.mockResolvedValue(mockChecks);
|
|
||||||
|
|
||||||
const result = await ConditionCheckService.getConditionChecks('rental-123');
|
|
||||||
|
|
||||||
expect(ConditionCheck.findAll).toHaveBeenCalledWith({
|
|
||||||
where: { rentalId: 'rental-123' },
|
|
||||||
include: [{
|
|
||||||
model: User,
|
|
||||||
as: 'submittedByUser',
|
|
||||||
attributes: ['id', 'firstName', 'lastName']
|
|
||||||
}],
|
|
||||||
order: [['submittedAt', 'ASC']]
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(result).toEqual(mockChecks);
|
|
||||||
expect(result.length).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
@@ -151,8 +151,7 @@ describe('API Namespaces', () => {
|
|||||||
it('has all condition check methods', () => {
|
it('has all condition check methods', () => {
|
||||||
const expectedMethods = [
|
const expectedMethods = [
|
||||||
'submitConditionCheck',
|
'submitConditionCheck',
|
||||||
'getConditionChecks',
|
'getBatchConditionChecks',
|
||||||
'getConditionCheckTimeline',
|
|
||||||
'getAvailableChecks',
|
'getAvailableChecks',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import DeclineRentalModal from "../components/DeclineRentalModal";
|
|||||||
import ConditionCheckModal from "../components/ConditionCheckModal";
|
import ConditionCheckModal from "../components/ConditionCheckModal";
|
||||||
import ConditionCheckViewerModal from "../components/ConditionCheckViewerModal";
|
import ConditionCheckViewerModal from "../components/ConditionCheckViewerModal";
|
||||||
import ReturnStatusModal from "../components/ReturnStatusModal";
|
import ReturnStatusModal from "../components/ReturnStatusModal";
|
||||||
|
import PaymentFailedModal from "../components/PaymentFailedModal";
|
||||||
|
|
||||||
const Owning: React.FC = () => {
|
const Owning: React.FC = () => {
|
||||||
// Helper function to format time
|
// Helper function to format time
|
||||||
@@ -73,16 +74,27 @@ const Owning: React.FC = () => {
|
|||||||
const [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
|
const [rentalForReturn, setRentalForReturn] = useState<Rental | null>(null);
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
const [itemToDelete, setItemToDelete] = useState<Item | null>(null);
|
const [itemToDelete, setItemToDelete] = useState<Item | null>(null);
|
||||||
|
const [showPaymentFailedModal, setShowPaymentFailedModal] = useState(false);
|
||||||
|
const [paymentFailedError, setPaymentFailedError] = useState<any>(null);
|
||||||
|
const [paymentFailedRental, setPaymentFailedRental] = useState<Rental | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchListings();
|
fetchListings();
|
||||||
fetchOwnerRentals();
|
fetchOwnerRentals();
|
||||||
fetchAvailableChecks();
|
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ownerRentals.length > 0) {
|
// Only fetch condition checks for rentals that will be displayed (pending/confirmed/active)
|
||||||
fetchConditionChecks();
|
const displayedRentals = ownerRentals.filter((r) =>
|
||||||
|
["pending", "confirmed", "active"].includes(r.displayStatus || r.status)
|
||||||
|
);
|
||||||
|
if (displayedRentals.length > 0) {
|
||||||
|
const rentalIds = displayedRentals.map((r) => r.id);
|
||||||
|
fetchConditionChecks(displayedRentals);
|
||||||
|
fetchAvailableChecks(rentalIds);
|
||||||
|
} else {
|
||||||
|
setConditionChecks([]);
|
||||||
|
setAvailableChecks([]);
|
||||||
}
|
}
|
||||||
}, [ownerRentals]);
|
}, [ownerRentals]);
|
||||||
|
|
||||||
@@ -154,9 +166,9 @@ const Owning: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchAvailableChecks = async () => {
|
const fetchAvailableChecks = async (rentalIds: string[]) => {
|
||||||
try {
|
try {
|
||||||
const response = await conditionCheckAPI.getAvailableChecks();
|
const response = await conditionCheckAPI.getAvailableChecks(rentalIds);
|
||||||
const checks = Array.isArray(response.data.availableChecks)
|
const checks = Array.isArray(response.data.availableChecks)
|
||||||
? response.data.availableChecks
|
? response.data.availableChecks
|
||||||
: [];
|
: [];
|
||||||
@@ -167,22 +179,15 @@ const Owning: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchConditionChecks = async () => {
|
const fetchConditionChecks = async (rentalsToFetch: Rental[]) => {
|
||||||
try {
|
try {
|
||||||
const allChecks: ConditionCheck[] = [];
|
if (rentalsToFetch.length === 0) {
|
||||||
for (const rental of ownerRentals) {
|
setConditionChecks([]);
|
||||||
try {
|
return;
|
||||||
const response = await conditionCheckAPI.getConditionChecks(
|
|
||||||
rental.id
|
|
||||||
);
|
|
||||||
if (response.data.conditionChecks) {
|
|
||||||
allChecks.push(...response.data.conditionChecks);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Skip rentals with no condition checks
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setConditionChecks(allChecks);
|
const rentalIds = rentalsToFetch.map((r) => r.id);
|
||||||
|
const response = await conditionCheckAPI.getBatchConditionChecks(rentalIds);
|
||||||
|
setConditionChecks(response.data.conditionChecks || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch condition checks:", err);
|
console.error("Failed to fetch condition checks:", err);
|
||||||
setConditionChecks([]);
|
setConditionChecks([]);
|
||||||
@@ -208,22 +213,29 @@ const Owning: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fetchOwnerRentals();
|
fetchOwnerRentals();
|
||||||
fetchAvailableChecks(); // Refresh available checks after rental confirmation
|
// Note: fetchAvailableChecks() removed - it will be triggered via ownerRentals useEffect
|
||||||
|
|
||||||
// Notify Navbar to update pending count
|
// Notify Navbar to update pending count
|
||||||
window.dispatchEvent(new CustomEvent("rentalStatusChanged"));
|
window.dispatchEvent(new CustomEvent("rentalStatusChanged"));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Failed to accept rental request:", err);
|
console.error("Failed to accept rental request:", err);
|
||||||
|
|
||||||
// Check if it's a payment failure
|
// Check if it's a payment failure (HTTP 402 or payment_failed error)
|
||||||
if (err.response?.data?.error?.includes("Payment failed")) {
|
if (
|
||||||
alert(
|
err.response?.status === 402 ||
|
||||||
`Payment failed during approval: ${
|
err.response?.data?.error === "payment_failed"
|
||||||
err.response.data.details || "Unknown payment error"
|
) {
|
||||||
}`
|
// Find the rental to get the item name
|
||||||
);
|
const rental = ownerRentals.find((r) => r.id === rentalId);
|
||||||
|
setPaymentFailedError(err.response.data);
|
||||||
|
setPaymentFailedRental(rental || null);
|
||||||
|
setShowPaymentFailedModal(true);
|
||||||
} else {
|
} else {
|
||||||
alert("Failed to accept rental request");
|
alert(
|
||||||
|
err.response?.data?.error ||
|
||||||
|
err.response?.data?.details ||
|
||||||
|
"Failed to accept rental request"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setIsProcessingPayment("");
|
setIsProcessingPayment("");
|
||||||
@@ -295,8 +307,13 @@ const Owning: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConditionCheckSuccess = () => {
|
const handleConditionCheckSuccess = () => {
|
||||||
fetchAvailableChecks();
|
// Refetch condition checks for displayed rentals
|
||||||
fetchConditionChecks();
|
const displayedRentals = ownerRentals.filter((r) =>
|
||||||
|
["pending", "confirmed", "active"].includes(r.displayStatus || r.status)
|
||||||
|
);
|
||||||
|
const rentalIds = displayedRentals.map((r) => r.id);
|
||||||
|
fetchAvailableChecks(rentalIds);
|
||||||
|
fetchConditionChecks(displayedRentals);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewConditionCheck = (check: ConditionCheck) => {
|
const handleViewConditionCheck = (check: ConditionCheck) => {
|
||||||
@@ -481,32 +498,44 @@ const Owning: React.FC = () => {
|
|||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
{rental.status === "pending" && (
|
{rental.status === "pending" && (
|
||||||
<>
|
<>
|
||||||
<button
|
{rental.paymentFailedNotifiedAt &&
|
||||||
className="btn btn-sm btn-success"
|
(!rental.paymentMethodUpdatedAt ||
|
||||||
onClick={() => handleAcceptRental(rental.id)}
|
new Date(rental.paymentFailedNotifiedAt) >
|
||||||
disabled={isProcessingPayment === rental.id}
|
new Date(rental.paymentMethodUpdatedAt)) ? (
|
||||||
>
|
<button
|
||||||
{isProcessingPayment === rental.id ? (
|
className="btn btn-sm btn-secondary"
|
||||||
<>
|
disabled
|
||||||
<div
|
>
|
||||||
className="spinner-border spinner-border-sm me-2"
|
Waiting for Payment Update
|
||||||
role="status"
|
</button>
|
||||||
>
|
) : (
|
||||||
<span className="visually-hidden">
|
<button
|
||||||
Loading...
|
className="btn btn-sm btn-success"
|
||||||
</span>
|
onClick={() => handleAcceptRental(rental.id)}
|
||||||
</div>
|
disabled={isProcessingPayment === rental.id}
|
||||||
Confirming...
|
>
|
||||||
</>
|
{isProcessingPayment === rental.id ? (
|
||||||
) : processingSuccess === rental.id ? (
|
<>
|
||||||
<>
|
<div
|
||||||
<i className="bi bi-check-circle me-1"></i>
|
className="spinner-border spinner-border-sm me-2"
|
||||||
Confirmed!
|
role="status"
|
||||||
</>
|
>
|
||||||
) : (
|
<span className="visually-hidden">
|
||||||
"Accept"
|
Loading...
|
||||||
)}
|
</span>
|
||||||
</button>
|
</div>
|
||||||
|
Confirming...
|
||||||
|
</>
|
||||||
|
) : processingSuccess === rental.id ? (
|
||||||
|
<>
|
||||||
|
<i className="bi bi-check-circle me-1"></i>
|
||||||
|
Confirmed!
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Accept"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-danger"
|
className="btn btn-sm btn-danger"
|
||||||
onClick={() => handleDeclineClick(rental)}
|
onClick={() => handleDeclineClick(rental)}
|
||||||
@@ -823,6 +852,21 @@ const Owning: React.FC = () => {
|
|||||||
conditionCheck={selectedConditionCheck}
|
conditionCheck={selectedConditionCheck}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Payment Failed Modal */}
|
||||||
|
{paymentFailedError && (
|
||||||
|
<PaymentFailedModal
|
||||||
|
show={showPaymentFailedModal}
|
||||||
|
onHide={() => {
|
||||||
|
setShowPaymentFailedModal(false);
|
||||||
|
setPaymentFailedError(null);
|
||||||
|
setPaymentFailedRental(null);
|
||||||
|
fetchOwnerRentals(); // Refresh to show updated button state
|
||||||
|
}}
|
||||||
|
paymentError={paymentFailedError}
|
||||||
|
itemName={paymentFailedRental?.item?.name || "Item"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Delete Confirmation Modal */}
|
{/* Delete Confirmation Modal */}
|
||||||
{showDeleteModal && (
|
{showDeleteModal && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -253,18 +253,14 @@ const Profile: React.FC = () => {
|
|||||||
(rental, index, self) => self.findIndex((r) => r.id === rental.id) === index
|
(rental, index, self) => self.findIndex((r) => r.id === rental.id) === index
|
||||||
);
|
);
|
||||||
|
|
||||||
const allChecks: ConditionCheck[] = [];
|
if (uniqueRentals.length === 0) {
|
||||||
for (const rental of uniqueRentals) {
|
setConditionChecks([]);
|
||||||
try {
|
return;
|
||||||
const response = await conditionCheckAPI.getConditionChecks(rental.id);
|
|
||||||
if (response.data.conditionChecks) {
|
|
||||||
allChecks.push(...response.data.conditionChecks);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Skip rentals with no condition checks
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setConditionChecks(allChecks);
|
|
||||||
|
const rentalIds = uniqueRentals.map((r) => r.id);
|
||||||
|
const response = await conditionCheckAPI.getBatchConditionChecks(rentalIds);
|
||||||
|
setConditionChecks(response.data.conditionChecks || []);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch condition checks:", err);
|
console.error("Failed to fetch condition checks:", err);
|
||||||
setConditionChecks([]);
|
setConditionChecks([]);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import ReviewItemModal from "../components/ReviewModal";
|
|||||||
import RentalCancellationModal from "../components/RentalCancellationModal";
|
import RentalCancellationModal from "../components/RentalCancellationModal";
|
||||||
import ConditionCheckModal from "../components/ConditionCheckModal";
|
import ConditionCheckModal from "../components/ConditionCheckModal";
|
||||||
import ConditionCheckViewerModal from "../components/ConditionCheckViewerModal";
|
import ConditionCheckViewerModal from "../components/ConditionCheckViewerModal";
|
||||||
|
import UpdatePaymentMethodModal from "../components/UpdatePaymentMethodModal";
|
||||||
|
|
||||||
const Renting: React.FC = () => {
|
const Renting: React.FC = () => {
|
||||||
// Helper function to format time
|
// Helper function to format time
|
||||||
@@ -59,15 +60,26 @@ const Renting: React.FC = () => {
|
|||||||
useState(false);
|
useState(false);
|
||||||
const [selectedConditionCheck, setSelectedConditionCheck] =
|
const [selectedConditionCheck, setSelectedConditionCheck] =
|
||||||
useState<ConditionCheck | null>(null);
|
useState<ConditionCheck | null>(null);
|
||||||
|
const [showUpdatePaymentModal, setShowUpdatePaymentModal] = useState(false);
|
||||||
|
const [rentalForPaymentUpdate, setRentalForPaymentUpdate] =
|
||||||
|
useState<Rental | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRentals();
|
fetchRentals();
|
||||||
fetchAvailableChecks();
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rentals.length > 0) {
|
// Only fetch condition checks for rentals that will be displayed (pending/confirmed/active)
|
||||||
fetchConditionChecks();
|
const displayedRentals = rentals.filter((r) =>
|
||||||
|
["pending", "confirmed", "active"].includes(r.displayStatus || r.status)
|
||||||
|
);
|
||||||
|
if (displayedRentals.length > 0) {
|
||||||
|
const rentalIds = displayedRentals.map((r) => r.id);
|
||||||
|
fetchConditionChecks(displayedRentals);
|
||||||
|
fetchAvailableChecks(rentalIds);
|
||||||
|
} else {
|
||||||
|
setConditionChecks([]);
|
||||||
|
setAvailableChecks([]);
|
||||||
}
|
}
|
||||||
}, [rentals]);
|
}, [rentals]);
|
||||||
|
|
||||||
@@ -82,9 +94,9 @@ const Renting: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchAvailableChecks = async () => {
|
const fetchAvailableChecks = async (rentalIds: string[]) => {
|
||||||
try {
|
try {
|
||||||
const response = await conditionCheckAPI.getAvailableChecks();
|
const response = await conditionCheckAPI.getAvailableChecks(rentalIds);
|
||||||
const checks = Array.isArray(response.data.availableChecks)
|
const checks = Array.isArray(response.data.availableChecks)
|
||||||
? response.data.availableChecks
|
? response.data.availableChecks
|
||||||
: [];
|
: [];
|
||||||
@@ -95,25 +107,15 @@ const Renting: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchConditionChecks = async () => {
|
const fetchConditionChecks = async (rentalsToFetch: Rental[]) => {
|
||||||
try {
|
try {
|
||||||
// Fetch condition checks for all rentals
|
if (rentalsToFetch.length === 0) {
|
||||||
const allChecks: any[] = [];
|
setConditionChecks([]);
|
||||||
for (const rental of rentals) {
|
return;
|
||||||
try {
|
|
||||||
const response = await conditionCheckAPI.getConditionChecks(
|
|
||||||
rental.id
|
|
||||||
);
|
|
||||||
const checks = Array.isArray(response.data.conditionChecks)
|
|
||||||
? response.data.conditionChecks
|
|
||||||
: [];
|
|
||||||
allChecks.push(...checks);
|
|
||||||
} catch (err) {
|
|
||||||
// Continue even if one rental fails
|
|
||||||
console.error(`Failed to fetch checks for rental ${rental.id}:`, err);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
setConditionChecks(allChecks);
|
const rentalIds = rentalsToFetch.map((r) => r.id);
|
||||||
|
const response = await conditionCheckAPI.getBatchConditionChecks(rentalIds);
|
||||||
|
setConditionChecks(response.data.conditionChecks || []);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error("Failed to fetch condition checks:", err);
|
console.error("Failed to fetch condition checks:", err);
|
||||||
setConditionChecks([]);
|
setConditionChecks([]);
|
||||||
@@ -136,6 +138,18 @@ const Renting: React.FC = () => {
|
|||||||
setRentalToCancel(null);
|
setRentalToCancel(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdatePaymentClick = (rental: Rental) => {
|
||||||
|
setRentalForPaymentUpdate(rental);
|
||||||
|
setShowUpdatePaymentModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePaymentMethodUpdated = () => {
|
||||||
|
// Refresh rentals to get updated data
|
||||||
|
fetchRentals();
|
||||||
|
setShowUpdatePaymentModal(false);
|
||||||
|
setRentalForPaymentUpdate(null);
|
||||||
|
};
|
||||||
|
|
||||||
const handleReviewClick = (rental: Rental) => {
|
const handleReviewClick = (rental: Rental) => {
|
||||||
setSelectedRental(rental);
|
setSelectedRental(rental);
|
||||||
setShowReviewModal(true);
|
setShowReviewModal(true);
|
||||||
@@ -152,8 +166,13 @@ const Renting: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConditionCheckSuccess = () => {
|
const handleConditionCheckSuccess = () => {
|
||||||
fetchAvailableChecks();
|
// Refetch condition checks for displayed rentals
|
||||||
fetchConditionChecks();
|
const displayedRentals = rentals.filter((r) =>
|
||||||
|
["pending", "confirmed", "active"].includes(r.displayStatus || r.status)
|
||||||
|
);
|
||||||
|
const rentalIds = displayedRentals.map((r) => r.id);
|
||||||
|
fetchAvailableChecks(rentalIds);
|
||||||
|
fetchConditionChecks(displayedRentals);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleViewConditionCheck = (check: ConditionCheck) => {
|
const handleViewConditionCheck = (check: ConditionCheck) => {
|
||||||
@@ -362,6 +381,26 @@ const Renting: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Payment Failed Alert */}
|
||||||
|
{(rental.displayStatus || rental.status) === "pending" &&
|
||||||
|
rental.paymentStatus === "pending" &&
|
||||||
|
rental.paymentFailedNotifiedAt && (
|
||||||
|
<div className="alert alert-warning py-2 mb-3">
|
||||||
|
<div className="d-flex align-items-center justify-content-between">
|
||||||
|
<small>
|
||||||
|
<strong>Payment issue:</strong> Please update your
|
||||||
|
payment method so the owner can approve your request.
|
||||||
|
</small>
|
||||||
|
<button
|
||||||
|
className="btn btn-sm btn-warning ms-2"
|
||||||
|
onClick={() => handleUpdatePaymentClick(rental)}
|
||||||
|
>
|
||||||
|
Update Payment
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="d-flex flex-column gap-2 mt-3">
|
<div className="d-flex flex-column gap-2 mt-3">
|
||||||
<div className="d-flex gap-2">
|
<div className="d-flex gap-2">
|
||||||
{((rental.displayStatus || rental.status) === "pending" ||
|
{((rental.displayStatus || rental.status) === "pending" ||
|
||||||
@@ -507,6 +546,20 @@ const Renting: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
conditionCheck={selectedConditionCheck}
|
conditionCheck={selectedConditionCheck}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Update Payment Method Modal */}
|
||||||
|
{rentalForPaymentUpdate && (
|
||||||
|
<UpdatePaymentMethodModal
|
||||||
|
show={showUpdatePaymentModal}
|
||||||
|
onHide={() => {
|
||||||
|
setShowUpdatePaymentModal(false);
|
||||||
|
setRentalForPaymentUpdate(null);
|
||||||
|
}}
|
||||||
|
rentalId={rentalForPaymentUpdate.id}
|
||||||
|
itemName={rentalForPaymentUpdate.item?.name || "Item"}
|
||||||
|
onSuccess={handlePaymentMethodUpdated}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -240,6 +240,8 @@ export const rentalAPI = {
|
|||||||
startDateTime: string;
|
startDateTime: string;
|
||||||
endDateTime: string;
|
endDateTime: string;
|
||||||
}) => api.post("/rentals/cost-preview", data),
|
}) => api.post("/rentals/cost-preview", data),
|
||||||
|
updatePaymentMethod: (id: string, stripePaymentMethodId: string) =>
|
||||||
|
api.put(`/rentals/${id}/payment-method`, { stripePaymentMethodId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const messageAPI = {
|
export const messageAPI = {
|
||||||
@@ -335,11 +337,14 @@ export const conditionCheckAPI = {
|
|||||||
rentalId: string,
|
rentalId: string,
|
||||||
data: { checkType: string; imageFilenames: string[]; notes?: string }
|
data: { checkType: string; imageFilenames: string[]; notes?: string }
|
||||||
) => api.post(`/condition-checks/${rentalId}`, data),
|
) => api.post(`/condition-checks/${rentalId}`, data),
|
||||||
getConditionChecks: (rentalId: string) =>
|
getBatchConditionChecks: (rentalIds: string[]) =>
|
||||||
api.get(`/condition-checks/${rentalId}`),
|
api.get(`/condition-checks/batch`, {
|
||||||
getConditionCheckTimeline: (rentalId: string) =>
|
params: { rentalIds: rentalIds.join(",") },
|
||||||
api.get(`/condition-checks/${rentalId}/timeline`),
|
}),
|
||||||
getAvailableChecks: () => api.get("/condition-checks"),
|
getAvailableChecks: (rentalIds: string[]) =>
|
||||||
|
api.get("/condition-checks", {
|
||||||
|
params: { rentalIds: rentalIds.join(",") },
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const feedbackAPI = {
|
export const feedbackAPI = {
|
||||||
|
|||||||
Reference in New Issue
Block a user