From bdfcd5537013ab4eb35db9b72f40ab02989dc384 Mon Sep 17 00:00:00 2001 From: Hanzo_dev <2002samudiojohan@gmail.com> Date: Tue, 3 Mar 2026 21:31:35 -0500 Subject: [PATCH] feat: business template/mold redesign - Supabase: Add schedule, whatsapp, instagram, facebook, gallery_images columns to businesses table - types/index.ts: Add 5 new fields to Business interface - businessService.ts: Update all SELECT queries to include new fields, add _parseFormData helper, handle gallery_images as JSON array - BusinessDetailsView.vue: Full redesign matching the approved mockup: * Hero 300px with centered name + yellow category badge * Horizontal scrollable quick-info pills (area, schedule, phone, web) * Image carousel (main image + gallery_images) with arrows + dots * About section with yellow left-accent bar * 2x2 social grid (WhatsApp, Instagram, Facebook, Maps) with brand colors * Coupon section preserved * Sticky CTA bar: Ver en el Mapa + Llamar buttons - PromoterDashboard.vue: Updated business modal with grouped sections (Info Basica / Portada / Descripcion / Redes / Galeria) + new fields --- frontend/src/services/businessService.ts | 35 +- frontend/src/types/index.ts | 6 + frontend/src/views/BusinessDetailsView.vue | 1170 ++++++++++++-------- frontend/src/views/PromoterDashboard.vue | 144 ++- 4 files changed, 858 insertions(+), 497 deletions(-) diff --git a/frontend/src/services/businessService.ts b/frontend/src/services/businessService.ts index 12bfc1e..5affc71 100644 --- a/frontend/src/services/businessService.ts +++ b/frontend/src/services/businessService.ts @@ -2,6 +2,8 @@ import { supabase } from '@/supabase' import type { Business } from '@/types' +const SELECT_FIELDS = 'id, name, address, phone, image_url, social_media, category, latitude, longitude, area, description, website, schedule, whatsapp, instagram, facebook, gallery_images, updated_at' + export const businessService = { /** Helper to upload file to supabase storage */ async uploadImage(file: File): Promise { @@ -18,58 +20,59 @@ export const businessService = { /** Get all businesses */ async getAllBusinesses(): Promise { - const { data, error } = await supabase.from('businesses').select('id, name, address, phone, image_url, social_media, category, latitude, longitude, area, description, website, updated_at') + const { data, error } = await supabase.from('businesses').select(SELECT_FIELDS) if (error) throw new Error(error.message) return data as Business[] }, /** Get a single business by ID */ async getBusiness(id: string): Promise { - const { data, error } = await supabase.from('businesses').select('id, name, address, phone, image_url, social_media, category, latitude, longitude, area, description, website, updated_at').eq('id', id).single() + const { data, error } = await supabase.from('businesses').select(SELECT_FIELDS).eq('id', id).single() if (error) throw new Error(error.message) return data as Business }, - /** Create a new business */ - async createBusiness(businessData: FormData): Promise { + /** Parse FormData into a payload object, handling gallery_images JSON */ + _parseFormData(businessData: FormData): { payload: any; fileUpload: File | null } { const payload: any = {} let fileUpload: File | null = null businessData.forEach((value, key) => { if ((key === 'file' || key === 'image') && value instanceof File) { fileUpload = value + } else if (key === 'gallery_images' && typeof value === 'string') { + // Comes as JSON string from the form + try { payload[key] = JSON.parse(value) } catch { /* ignore */ } } else if (value !== 'null' && value !== '' && key !== 'file' && key !== 'image') { payload[key] = value } }) + return { payload, fileUpload } + }, + + /** Create a new business */ + async createBusiness(businessData: FormData): Promise { + const { payload, fileUpload } = this._parseFormData(businessData) + if (fileUpload) { payload.image_url = await this.uploadImage(fileUpload) } - const { data, error } = await supabase.from('businesses').insert([payload]).select().single() + const { data, error } = await supabase.from('businesses').insert([payload]).select(SELECT_FIELDS).single() if (error) throw new Error(error.message) return data as Business }, /** Update an existing business */ async updateBusiness(id: string, businessData: FormData): Promise { - const payload: any = {} - let fileUpload: File | null = null - - businessData.forEach((value, key) => { - if ((key === 'file' || key === 'image') && value instanceof File) { - fileUpload = value - } else if (value !== 'null' && value !== '' && key !== 'file' && key !== 'image') { - payload[key] = value - } - }) + const { payload, fileUpload } = this._parseFormData(businessData) if (fileUpload) { payload.image_url = await this.uploadImage(fileUpload) } - const { data, error } = await supabase.from('businesses').update(payload).eq('id', id).select().single() + const { data, error } = await supabase.from('businesses').update(payload).eq('id', id).select(SELECT_FIELDS).single() if (error) throw new Error(error.message) return data as Business }, diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index e5e593c..1d3fe86 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -97,6 +97,12 @@ export interface Business { area?: string | null description?: string | null website?: string | null + // Template/Mold fields + schedule?: string | null // "Lun-SΓ‘b 8am-10pm" + whatsapp?: string | null // WhatsApp number + instagram?: string | null // @handle + facebook?: string | null // page URL or username + gallery_images?: string[] // carousel photo URLs updated_at?: string } diff --git a/frontend/src/views/BusinessDetailsView.vue b/frontend/src/views/BusinessDetailsView.vue index 151bd95..223536e 100644 --- a/frontend/src/views/BusinessDetailsView.vue +++ b/frontend/src/views/BusinessDetailsView.vue @@ -1,7 +1,6 @@ ο»Ώ diff --git a/frontend/src/views/PromoterDashboard.vue b/frontend/src/views/PromoterDashboard.vue index 61ad03c..26042d3 100644 --- a/frontend/src/views/PromoterDashboard.vue +++ b/frontend/src/views/PromoterDashboard.vue @@ -56,7 +56,13 @@ const currentBusiness = ref>({ category: 'Restaurante', area: 'Boquete', description: '', - website: '' + website: '', + // Template fields + schedule: '', + whatsapp: '', + instagram: '', + facebook: '', + gallery_images: [] }) const userName = localStorage.getItem('user_name') || 'Promotor' @@ -179,7 +185,12 @@ function openCreateBusinessModal() { category: 'Restaurante', area: 'Boquete', description: '', - website: '' + website: '', + schedule: '', + whatsapp: '', + instagram: '', + facebook: '', + gallery_images: [] } showBusinessModal.value = true businessImageFile.value = null @@ -214,7 +225,16 @@ async function saveBusiness() { formData.append('area', currentBusiness.value.area || 'Boquete') formData.append('description', currentBusiness.value.description || '') formData.append('website', currentBusiness.value.website || '') - + // Template fields + formData.append('schedule', currentBusiness.value.schedule || '') + formData.append('whatsapp', currentBusiness.value.whatsapp || '') + formData.append('instagram', currentBusiness.value.instagram || '') + formData.append('facebook', currentBusiness.value.facebook || '') + // gallery_images handled separately via JSON + if (currentBusiness.value.gallery_images?.length) { + formData.append('gallery_images', JSON.stringify(currentBusiness.value.gallery_images)) + } + if (businessImageFile.value) { formData.append('image', businessImageFile.value) } @@ -692,39 +712,57 @@ async function toggleCouponStatus(coupon: Coupon) {
+ + + +
- - + +
-
- - -
+
+
+ + +
+
+ + +
+
+ + +
- +
@@ -732,20 +770,55 @@ async function toggleCouponStatus(coupon: Coupon) {
+ + +
- - + +
+ + + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
- - -
-
- - + + + Agrega URLs de imΓ‘genes para el carrusel (menΓΊ, ambiente, experiencias). Una URL por lΓ­nea.
+
- +
@@ -1182,6 +1255,35 @@ async function toggleCouponStatus(coupon: Coupon) { .submit-btn:hover { opacity: 0.9; } +/* Form section labels (organize modal into visual sections) */ +.form-section-label { + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--active-color); + padding: 8px 0 2px; + border-bottom: 1px solid var(--border-color); + margin-top: 4px; +} + +.form-hint { + font-size: 0.75rem; + color: var(--text-secondary); + line-height: 1.5; + margin-top: 4px; +} + +/* Submit btn icon alignment */ +.submit-btn { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} + +.submit-btn .material-icons { font-size: 1.1rem; } + .spin { animation: spin 1s linear infinite; }