diff --git a/502.html b/502.html index 479e97e..57f97a3 100644 --- a/502.html +++ b/502.html @@ -129,7 +129,7 @@

The application is currently launching. The page will automatically refresh once site is available.

-

Fix It Local

+

Fix-It-Local

Trust-first service directory with verification, transparent pricing, AI-style matching, and request tracking.

diff --git a/README.md b/README.md index f95f064..93b1c09 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Fix It Local +# Fix-It-Local ## This project was generated by [Flatlogic Platform](https://flatlogic.com). diff --git a/assets/pasted-20260218-034356-d8337609.png b/assets/pasted-20260218-034356-d8337609.png new file mode 100644 index 0000000..8f07b54 Binary files /dev/null and b/assets/pasted-20260218-034356-d8337609.png differ diff --git a/backend/README.md b/backend/README.md index 7117b49..5692603 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,5 +1,5 @@ -#Fix It Local - template backend, +#Fix-It-Local - template backend, #### Run App on local machine: diff --git a/backend/package.json b/backend/package.json index d41e65c..ea14ee8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,6 @@ { "name": "craftednetwork", - "description": "Fix It Local - template backend", + "description": "Fix-It-Local - template backend", "scripts": { "start": "npm run db:migrate && npm run db:seed && npm run watch", "lint": "eslint . --ext .js", diff --git a/backend/src/config.js b/backend/src/config.js index dbb27e2..b660d30 100644 --- a/backend/src/config.js +++ b/backend/src/config.js @@ -37,7 +37,7 @@ const config = { }, uploadDir: os.tmpdir(), email: { - from: 'Fix It Local ', + from: 'Fix-It-Local ', host: 'email-smtp.us-east-1.amazonaws.com', port: 587, auth: { @@ -67,11 +67,11 @@ const config = { config.pexelsKey = process.env.PEXELS_KEY || ''; -config.pexelsQuery = 'Crafted bridge over calm river'; +config.pexelsQuery = 'home repair services'; config.host = process.env.NODE_ENV === "production" ? config.remote : "http://localhost"; config.apiUrl = `${config.host}${config.port ? `:${config.port}` : ``}/api`; config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`; config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`; config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`; -module.exports = config; \ No newline at end of file +module.exports = config; diff --git a/backend/src/db/api/businesses.js b/backend/src/db/api/businesses.js index bad14e3..5dd519c 100644 --- a/backend/src/db/api/businesses.js +++ b/backend/src/db/api/businesses.js @@ -37,11 +37,11 @@ module.exports = class BusinessesDBApi { if (!isAdmin && !isPublicOrConsumer) { // This is a "client" (e.g. Verified Business Owner) - if (currentUser.businessId) { - where.id = currentUser.businessId; - } else { - where.owner_userId = currentUser.id; - } + // Show businesses they own OR their primary businessId + where[Op.or] = [ + { owner_userId: currentUser.id }, + { id: currentUser.businessId || null } + ]; } else if (isPublicOrConsumer) { where.is_active = true; } diff --git a/backend/src/db/api/lead_matches.js b/backend/src/db/api/lead_matches.js index 9b9cff0..c341eff 100644 --- a/backend/src/db/api/lead_matches.js +++ b/backend/src/db/api/lead_matches.js @@ -17,7 +17,7 @@ module.exports = class Lead_matchesDBApi { const currentUser = options?.currentUser; const transaction = (options && options.transaction) || undefined; - // Data Isolation for Fix It Local™ + // Data Isolation for Fix-It-Local™ if (currentUser && currentUser.app_role) { const roleName = currentUser.app_role.name; if (roleName === 'Verified Business Owner') { diff --git a/backend/src/index.js b/backend/src/index.js index d4d480a..5bb8609 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -77,8 +77,8 @@ const options = { openapi: "3.0.0", info: { version: "1.0.0", - title: "Fix It Local", - description: "Fix It Local Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.", + title: "Fix-It-Local", + description: "Fix-It-Local Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.", }, servers: [ { diff --git a/backend/src/routes/businesses.js b/backend/src/routes/businesses.js index 255bc44..56ac245 100644 --- a/backend/src/routes/businesses.js +++ b/backend/src/routes/businesses.js @@ -480,11 +480,10 @@ router.get('/autocomplete', async (req, res) => { * description: Some server error */ router.get('/:id', wrapAsync(async (req, res) => { - const payload = await BusinessesDBApi.findBy( - { id: req.params.id }, + const payload = await BusinessesService.findBy( + req.params.id, + req.currentUser ); - - res.status(200).send(payload); })); diff --git a/backend/src/services/businesses.js b/backend/src/services/businesses.js index 9c8cd5b..ccc4ba3 100644 --- a/backend/src/services/businesses.js +++ b/backend/src/services/businesses.js @@ -7,14 +7,53 @@ const csv = require('csv-parser'); const axios = require('axios'); const config = require('../config'); const stream = require('stream'); +const { v4: uuidv4 } = require('uuid'); module.exports = class BusinessesService { + static _sanitize(data) { + const numericFields = ['lat', 'lng', 'reliability_score', 'response_time_median_minutes', 'rating']; + numericFields.forEach(field => { + if (data[field] === '') { + data[field] = null; + } + }); + return data; + } + + static async findBy(id, currentUser) { + const business = await BusinessesDBApi.findBy({ id }); + + if (!business) { + throw new ValidationError('businessesNotFound'); + } + + // 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'); + } + } + + return business; + } + static async create(data, currentUser) { const transaction = await db.sequelize.transaction(); try { + data = this._sanitize(data); + // For VBOs, force the owner to be the current user - if (currentUser.app_role?.name === 'Verified Business Owner') { + if (currentUser?.app_role?.name === 'Verified Business Owner') { data.owner_user = currentUser.id; + data.is_active = true; // Ensure new business owner listings are active + + // Auto-generate internal fields if missing + if (!data.slug && data.name) { + data.slug = data.name.toLowerCase().replace(/ /g, '-').replace(/[^\w-]+/g, '') + '-' + uuidv4().substring(0, 4); + } + if (!data.tenant_key) { + data.tenant_key = 'TENANT-' + uuidv4().substring(0, 8).toUpperCase(); + } } const business = await BusinessesDBApi.create( @@ -26,7 +65,7 @@ module.exports = class BusinessesService { ); // Link business to user if they don't have one set yet - if (currentUser.app_role?.name === 'Verified Business Owner' && !currentUser.businessId) { + if (currentUser?.app_role?.name === 'Verified Business Owner' && !currentUser.businessId) { await db.users.update({ businessId: business.id }, { where: { id: currentUser.id }, transaction @@ -58,7 +97,7 @@ module.exports = class BusinessesService { }, { transaction }); // Link business to user if they don't have one set yet - if (!currentUser.businessId) { + if (currentUser && !currentUser.businessId) { await db.users.update({ businessId: business.id }, { where: { id: currentUser.id }, transaction @@ -111,6 +150,8 @@ module.exports = class BusinessesService { static async update(data, id, currentUser) { const transaction = await db.sequelize.transaction(); try { + data = this._sanitize(data); + let business = await BusinessesDBApi.findBy( {id}, {transaction}, @@ -123,13 +164,15 @@ module.exports = class BusinessesService { } // Ownership check for Verified Business Owner - if (currentUser.app_role?.name === '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'); } // Prevent transferring ownership delete data.owner_user; delete data.owner_userId; + delete data.slug; + delete data.tenant_key; } const updatedBusinesses = await BusinessesDBApi.update( @@ -155,7 +198,7 @@ module.exports = class BusinessesService { try { // Ownership check for Verified Business Owner - if (currentUser.app_role?.name === 'Verified Business Owner') { + if (currentUser?.app_role?.name === 'Verified Business Owner') { const records = await db.businesses.findAll({ where: { id: { [db.Sequelize.Op.in]: ids }, @@ -190,7 +233,7 @@ module.exports = class BusinessesService { let business = await db.businesses.findByPk(id, { transaction }); if (!business) throw new ValidationError('businessesNotFound'); - if (currentUser.app_role?.name === 'Verified Business Owner' && business.owner_userId !== currentUser.id && business.id !== currentUser.businessId) { + if (currentUser?.app_role?.name === 'Verified Business Owner' && business.owner_userId !== currentUser.id && business.id !== currentUser.businessId) { throw new ForbiddenError('forbidden'); } @@ -210,4 +253,4 @@ module.exports = class BusinessesService { } -}; +}; \ No newline at end of file diff --git a/backend/src/services/notifications/list.js b/backend/src/services/notifications/list.js index cb05dff..d2ace73 100644 --- a/backend/src/services/notifications/list.js +++ b/backend/src/services/notifications/list.js @@ -1,6 +1,6 @@ const errors = { app: { - title: 'Fix It Local', + title: 'Fix-It-Local', }, auth: { diff --git a/backend/src/services/search.js b/backend/src/services/search.js index 7830218..f3058b2 100644 --- a/backend/src/services/search.js +++ b/backend/src/services/search.js @@ -57,6 +57,9 @@ module.exports = class SearchService { throw new ValidationError('iam.errors.searchQueryRequired'); } + const roleName = currentUser?.app_role?.name || 'Public'; + const isAdmin = roleName === 'Administrator' || roleName === 'Platform Owner'; + // Columns that can be searched using iLike const searchableColumns = { "users": [ @@ -140,6 +143,12 @@ module.exports = class SearchService { [Op.or]: searchConditions, }; + // Only show active businesses for non-admins + if (tableName === 'businesses' && !isAdmin) { + whereCondition[Op.and] = whereCondition[Op.and] || []; + whereCondition[Op.and].push({ is_active: true }); + } + // If location is provided, bias local results by location for businesses and locations if (location && (tableName === 'businesses' || tableName === 'locations')) { const locationConditions = [ @@ -153,11 +162,10 @@ module.exports = class SearchService { locationConditions.push({ address: { [Op.iLike]: `%${location}%` } }); } - whereCondition[Op.and] = [ - { + whereCondition[Op.and] = whereCondition[Op.and] || []; + whereCondition[Op.and].push({ [Op.or]: locationConditions - } - ]; + }); } const hasPerm = await checkPermissions(`READ_${tableName.toUpperCase()}`, currentUser); @@ -215,6 +223,7 @@ module.exports = class SearchService { if (foundCategories.length > 0) { const categoryIds = foundCategories.map(c => c.id); const businessesInCategories = await db.businesses.findAll({ + where: !isAdmin ? { is_active: true } : {}, include: [ { model: db.business_categories, diff --git a/frontend/README.md b/frontend/README.md index 269d0f7..a76c2e4 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,4 +1,4 @@ -# Fix It Local +# Fix-It-Local ## This project was generated by Flatlogic Platform. ## Install diff --git a/frontend/public/logo.png b/frontend/public/logo.png new file mode 100644 index 0000000..8f07b54 Binary files /dev/null and b/frontend/public/logo.png differ diff --git a/frontend/src/components/AsideMenuLayer.tsx b/frontend/src/components/AsideMenuLayer.tsx index 330a2ad..a41a58b 100644 --- a/frontend/src/components/AsideMenuLayer.tsx +++ b/frontend/src/components/AsideMenuLayer.tsx @@ -5,6 +5,7 @@ import AsideMenuList from './AsideMenuList' import { MenuAsideItem } from '../interfaces' import { useAppSelector } from '../stores/hooks' import Link from 'next/link'; +import Logo from './Logo' type Props = { @@ -37,11 +38,10 @@ export default function AsideMenuLayer({ menu, className = '', ...props }: Props
-
- - Fix It Local - - +
+ + +
- ))} +
+ + handleSubmit(values)} + > + {({ values, setFieldValue }) => ( + +
+

Overall Experience

+
+ {[1, 2, 3, 4, 5].map((star) => ( + + ))} +
+
+ {values.rating === 1 && 'Poor'} + {values.rating === 2 && 'Fair'} + {values.rating === 3 && 'Good'} + {values.rating === 4 && 'Very Good'} + {values.rating === 5 && 'Excellent!'} +
-
- {values.rating === 1 && 'Poor'} - {values.rating === 2 && 'Fair'} - {values.rating === 3 && 'Good'} - {values.rating === 4 && 'Very Good'} - {values.rating === 5 && 'Excellent!'} -
-
- - - + + + - + - - - businessId ? router.push(`/public/businesses-details?id=${businessId}`) : router.push('/reviews/reviews-list')} - className="w-full md:w-auto px-12 py-4 rounded-2xl" - /> - - - )} - - -
- + + + businessId ? router.push(`/public/businesses-details?id=${businessId}`) : router.push('/reviews/reviews-list')} + className="w-full md:w-auto px-12 py-4 rounded-2xl" + disabled={isSubmitting} + /> + + + )} + + +
+ +
+ ) } ReviewsNew.getLayout = function getLayout(page: ReactElement) { return ( - + {page} - + ) } diff --git a/frontend/src/pages/terms-of-use.tsx b/frontend/src/pages/terms-of-use.tsx index 829e0c2..8b98088 100644 --- a/frontend/src/pages/terms-of-use.tsx +++ b/frontend/src/pages/terms-of-use.tsx @@ -5,7 +5,7 @@ import LayoutGuest from '../layouts/Guest'; import { getPageTitle } from '../config'; export default function PrivacyPolicy() { - const title = 'Fix It Local'; + const title = 'Fix-It-Local'; const [projectUrl, setProjectUrl] = useState(''); useEffect(() => {