carregar imagem para barbearia

This commit is contained in:
2026-02-26 17:38:31 +00:00
parent 2c78a28a3e
commit 8c35b8b6e0
2 changed files with 129 additions and 10 deletions

View File

@@ -44,6 +44,7 @@ type AppContextValue = State & {
addBarber: (shopId: string, barber: Omit<Barber, 'id'>) => void;
updateBarber: (shopId: string, barber: Barber) => void;
deleteBarber: (shopId: string, barberId: string) => void;
updateShopDetails: (shopId: string, payload: Partial<BarberShop>) => Promise<void>;
refreshShops: () => Promise<void>;
};
@@ -455,6 +456,25 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
}));
};
const updateShopDetails: AppContextValue['updateShopDetails'] = async (shopId, payload) => {
// DB Update
const { error } = await supabase
.from('shops')
.update(payload)
.eq('id', shopId);
if (error) {
console.error('Failed to update shop details:', error);
return;
}
// Local State Update
setState((s) => ({
...s,
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, ...payload } : shop)),
}));
};
const value: AppContextValue = {
...state,
login,
@@ -478,6 +498,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
addBarber,
updateBarber,
deleteBarber,
updateShopDetails,
refreshShops,
};

View File

@@ -12,6 +12,7 @@ import { Badge } from '../components/ui/badge';
import { Tabs } from '../components/ui/tabs';
import { currency } from '../lib/format';
import { useApp } from '../context/AppContext';
import { supabase } from '../lib/supabase';
import { Product } from '../types';
import { BarChart, Bar, CartesianGrid, ResponsiveContainer, Tooltip, XAxis } from 'recharts';
import { CalendarWeekView } from '../components/CalendarWeekView';
@@ -67,7 +68,7 @@ const periods: Record<string, (date: Date) => boolean> = {
const parseDate = (value: string) => new Date(value.replace(' ', 'T'));
type TabId = 'overview' | 'appointments' | 'history' | 'orders' | 'services' | 'products' | 'barbers';
type TabId = 'overview' | 'appointments' | 'history' | 'orders' | 'services' | 'products' | 'barbers' | 'settings';
export default function Dashboard() {
// Importa estado global interligado com Supabase (ex: updateAppointmentStatus reflete-se na BD)
@@ -86,6 +87,7 @@ export default function Dashboard() {
deleteProduct,
deleteService,
deleteBarber,
updateShopDetails,
} = useApp();
const shop = shops.find((s) => s.id === user?.shopId);
@@ -276,6 +278,38 @@ export default function Dashboard() {
setBarberSpecs('');
};
const [isUploading, setIsUploading] = useState(false);
const handleImageUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !shop) return;
setIsUploading(true);
try {
const fileExt = file.name.split('.').pop();
const fileName = `${shop.id}-${Math.random()}.${fileExt}`;
const filePath = `covers/${fileName}`;
const { error: uploadError } = await supabase.storage
.from('shops')
.upload(filePath, file);
if (uploadError) throw uploadError;
const { data } = supabase.storage
.from('shops')
.getPublicUrl(filePath);
await updateShopDetails(shop.id, { imageUrl: data.publicUrl });
alert('Foto de capa atualizada com sucesso!');
} catch (error) {
console.error('Erro ao fazer upload da imagem:', error);
alert('Erro ao fazer upload. Verifique se criou o bucket "shops" corretamente no Supabase.');
} finally {
setIsUploading(false);
}
};
const tabs = [
{ id: 'overview' as TabId, label: 'Visão Geral', icon: BarChart3 },
{ id: 'appointments' as TabId, label: 'Agendamentos', icon: Calendar },
@@ -284,6 +318,7 @@ export default function Dashboard() {
{ id: 'services' as TabId, label: 'Serviços', icon: Scissors },
{ id: 'products' as TabId, label: 'Produtos', icon: Package },
{ id: 'barbers' as TabId, label: 'Barbeiros', icon: Users },
{ id: 'settings' as TabId, label: 'Definições', icon: Settings },
];
return (
@@ -300,8 +335,8 @@ export default function Dashboard() {
key={p}
onClick={() => setPeriod(p)}
className={`px-3 py-1.5 rounded-lg text-xs font-medium border transition-all ${period === p
? 'border-amber-500 bg-amber-50 text-amber-700 shadow-sm'
: 'border-slate-200 text-slate-700 hover:border-amber-300 hover:bg-amber-50/50'
? 'border-amber-500 bg-amber-50 text-amber-700 shadow-sm'
: 'border-slate-200 text-slate-700 hover:border-amber-300 hover:bg-amber-50/50'
}`}
>
{p === 'mes' ? 'Mês' : p === 'hoje' ? 'Hoje' : p === 'semana' ? 'Semana' : 'Total'}
@@ -488,7 +523,9 @@ export default function Dashboard() {
</div>
<span className="text-sm font-medium text-slate-900">Calendário</span>
</button>
<button className="flex flex-col items-center gap-2 p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors">
<button
onClick={() => setActiveTab('settings')}
className="flex flex-col items-center gap-2 p-4 border border-slate-200 rounded-lg hover:bg-slate-50 transition-colors">
<div className="p-2 bg-purple-100 rounded-lg">
<Settings size={20} className="text-purple-600" />
</div>
@@ -559,8 +596,8 @@ export default function Dashboard() {
<button
onClick={() => setAppointmentView('list')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${appointmentView === 'list'
? 'border-indigo-500 bg-indigo-50 text-indigo-700 font-semibold'
: 'border-slate-200 text-slate-700 hover:border-indigo-300 hover:bg-indigo-50/50'
? 'border-indigo-500 bg-indigo-50 text-indigo-700 font-semibold'
: 'border-slate-200 text-slate-700 hover:border-indigo-300 hover:bg-indigo-50/50'
}`}
>
<List size={18} />
@@ -569,8 +606,8 @@ export default function Dashboard() {
<button
onClick={() => setAppointmentView('calendar')}
className={`flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${appointmentView === 'calendar'
? 'border-indigo-500 bg-indigo-50 text-indigo-700 font-semibold'
: 'border-slate-200 text-slate-700 hover:border-indigo-300 hover:bg-indigo-50/50'
? 'border-indigo-500 bg-indigo-50 text-indigo-700 font-semibold'
: 'border-slate-200 text-slate-700 hover:border-indigo-300 hover:bg-indigo-50/50'
}`}
>
<Calendar size={18} />
@@ -668,8 +705,8 @@ export default function Dashboard() {
<button
onClick={() => setIncludeCancelled(!includeCancelled)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg border-2 transition-all ${includeCancelled
? 'border-indigo-500 bg-indigo-50 text-indigo-700'
: 'border-slate-200 text-slate-700 hover:border-indigo-300'
? 'border-indigo-500 bg-indigo-50 text-indigo-700'
: 'border-slate-200 text-slate-700 hover:border-indigo-300'
}`}
>
<span>Incluir cancelamentos</span>
@@ -1080,6 +1117,67 @@ export default function Dashboard() {
</Card>
</div>
)}
{activeTab === 'settings' && (
<div className="space-y-6">
<Card className="p-6">
<h2 className="text-lg font-bold text-slate-900 mb-6">Definições da Barbearia</h2>
<div className="space-y-6 max-w-2xl">
<div>
<h3 className="text-sm font-semibold text-slate-900 mb-2">Foto de Capa</h3>
<p className="text-sm text-slate-500 mb-4">
Esta foto será exibida na página de detalhes da sua barbearia para todos os clientes.
</p>
<div className="flex flex-col gap-4">
{shop.imageUrl ? (
<div className="relative h-48 w-full md:w-96 rounded-lg overflow-hidden border border-slate-200">
<img
src={shop.imageUrl}
alt="Capa da Barbearia"
className="w-full h-full object-cover"
/>
</div>
) : (
<div className="h-48 w-full md:w-96 rounded-lg border-2 border-dashed border-slate-300 bg-slate-50 flex flex-col items-center justify-center text-slate-500">
<Globe size={32} className="mb-2 text-slate-400" />
<p className="text-sm font-medium">Sem foto de capa</p>
</div>
)}
<div>
<input
type="file"
id="cover-upload"
className="hidden"
accept="image/png, image/jpeg, image/webp"
onChange={handleImageUpload}
disabled={isUploading}
/>
<label
htmlFor="cover-upload"
className={`inline-flex items-center justify-center rounded-lg bg-indigo-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 cursor-pointer transition-colors ${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`}
>
{isUploading ? (
<div className="flex items-center gap-2">
<RefreshCw size={16} className="animate-spin" />
<span>A carregar...</span>
</div>
) : (
<div className="flex items-center gap-2">
<Store size={16} />
<span>{shop.imageUrl ? 'Alterar Foto' : 'Carregar Foto'}</span>
</div>
)}
</label>
</div>
</div>
</div>
</div>
</Card>
</div>
)}
</div>
);
}