carregar imagem para barbearia
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user