mirror of
https://github.com/domfelipe/mika-agent-assist.git
synced 2026-08-07 08:36:44 +00:00
Adicionou seletor de fuso
X-Lovable-Edit-ID: edt-ef99bd09-2010-4479-8440-eaf82123d48b Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com>
This commit is contained in:
commit
2d72313281
3 changed files with 198 additions and 2 deletions
|
|
@ -15,6 +15,7 @@ export interface Profile {
|
|||
stripe_customer_id: string | null;
|
||||
paddle_customer_id: string | null;
|
||||
onboarding_completed: boolean;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export function useProfile() {
|
||||
|
|
|
|||
55
src/lib/timezones.ts
Normal file
55
src/lib/timezones.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
export interface TimezoneOption {
|
||||
value: string;
|
||||
label: string;
|
||||
group: string;
|
||||
}
|
||||
|
||||
export const TIMEZONE_OPTIONS: TimezoneOption[] = [
|
||||
// Brasil
|
||||
{ value: "America/Sao_Paulo", label: "Brasília (GMT-3) — São Paulo, Rio, BH", group: "Brasil" },
|
||||
{ value: "America/Bahia", label: "Salvador (GMT-3)", group: "Brasil" },
|
||||
{ value: "America/Fortaleza", label: "Fortaleza (GMT-3)", group: "Brasil" },
|
||||
{ value: "America/Recife", label: "Recife (GMT-3)", group: "Brasil" },
|
||||
{ value: "America/Belem", label: "Belém (GMT-3)", group: "Brasil" },
|
||||
{ value: "America/Manaus", label: "Manaus (GMT-4) — Amazonas", group: "Brasil" },
|
||||
{ value: "America/Cuiaba", label: "Cuiabá (GMT-4) — Mato Grosso", group: "Brasil" },
|
||||
{ value: "America/Porto_Velho", label: "Porto Velho (GMT-4) — Rondônia", group: "Brasil" },
|
||||
{ value: "America/Boa_Vista", label: "Boa Vista (GMT-4) — Roraima", group: "Brasil" },
|
||||
{ value: "America/Rio_Branco", label: "Rio Branco (GMT-5) — Acre", group: "Brasil" },
|
||||
{ value: "America/Noronha", label: "Fernando de Noronha (GMT-2)", group: "Brasil" },
|
||||
|
||||
// Américas
|
||||
{ value: "America/New_York", label: "Nova York (GMT-5/-4)", group: "Américas" },
|
||||
{ value: "America/Chicago", label: "Chicago (GMT-6/-5)", group: "Américas" },
|
||||
{ value: "America/Denver", label: "Denver (GMT-7/-6)", group: "Américas" },
|
||||
{ value: "America/Los_Angeles", label: "Los Angeles (GMT-8/-7)", group: "Américas" },
|
||||
{ value: "America/Toronto", label: "Toronto (GMT-5/-4)", group: "Américas" },
|
||||
{ value: "America/Mexico_City", label: "Cidade do México (GMT-6)", group: "Américas" },
|
||||
{ value: "America/Buenos_Aires", label: "Buenos Aires (GMT-3)", group: "Américas" },
|
||||
{ value: "America/Santiago", label: "Santiago (GMT-4/-3)", group: "Américas" },
|
||||
{ value: "America/Bogota", label: "Bogotá (GMT-5)", group: "Américas" },
|
||||
{ value: "America/Lima", label: "Lima (GMT-5)", group: "Américas" },
|
||||
|
||||
// Europa
|
||||
{ value: "Europe/London", label: "Londres (GMT+0/+1)", group: "Europa" },
|
||||
{ value: "Europe/Lisbon", label: "Lisboa (GMT+0/+1)", group: "Europa" },
|
||||
{ value: "Europe/Madrid", label: "Madri (GMT+1/+2)", group: "Europa" },
|
||||
{ value: "Europe/Paris", label: "Paris (GMT+1/+2)", group: "Europa" },
|
||||
{ value: "Europe/Berlin", label: "Berlim (GMT+1/+2)", group: "Europa" },
|
||||
{ value: "Europe/Rome", label: "Roma (GMT+1/+2)", group: "Europa" },
|
||||
{ value: "Europe/Amsterdam", label: "Amsterdã (GMT+1/+2)", group: "Europa" },
|
||||
{ value: "Europe/Moscow", label: "Moscou (GMT+3)", group: "Europa" },
|
||||
|
||||
// Ásia / Oceania / África
|
||||
{ value: "Asia/Dubai", label: "Dubai (GMT+4)", group: "Outros" },
|
||||
{ value: "Asia/Kolkata", label: "Mumbai / Nova Délhi (GMT+5:30)", group: "Outros" },
|
||||
{ value: "Asia/Shanghai", label: "Xangai (GMT+8)", group: "Outros" },
|
||||
{ value: "Asia/Tokyo", label: "Tóquio (GMT+9)", group: "Outros" },
|
||||
{ value: "Australia/Sydney", label: "Sydney (GMT+10/+11)", group: "Outros" },
|
||||
{ value: "Africa/Johannesburg", label: "Joanesburgo (GMT+2)", group: "Outros" },
|
||||
{ value: "UTC", label: "UTC (GMT+0)", group: "Outros" },
|
||||
];
|
||||
|
||||
export function getTimezoneLabel(value: string): string {
|
||||
return TIMEZONE_OPTIONS.find((t) => t.value === value)?.label ?? value;
|
||||
}
|
||||
|
|
@ -1,14 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { z } from "zod";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { IMaskInput } from "react-imask";
|
||||
import { toast } from "sonner";
|
||||
import { Loader2, LogOut } from "lucide-react";
|
||||
import { Loader2, LogOut, Globe } from "lucide-react";
|
||||
import { supabase } from "@/integrations/supabase/client";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useProfile } from "@/hooks/use-profile";
|
||||
|
|
@ -17,6 +17,16 @@ import { Button } from "@/components/ui/button";
|
|||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { TIMEZONE_OPTIONS } from "@/lib/timezones";
|
||||
|
||||
export const Route = createFileRoute("/painel/configuracoes")({
|
||||
component: SettingsPage,
|
||||
|
|
@ -144,6 +154,8 @@ function SettingsPage() {
|
|||
</form>
|
||||
</section>
|
||||
|
||||
<TimezoneSection currentTimezone={profile?.timezone ?? "America/Sao_Paulo"} userId={user?.id} />
|
||||
|
||||
<section className="rounded-xl border border-border bg-card p-6 shadow-soft">
|
||||
<h2 className="text-lg font-semibold mb-1">Segurança</h2>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
|
|
@ -157,3 +169,131 @@ function SettingsPage() {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TimezoneSection({
|
||||
currentTimezone,
|
||||
userId,
|
||||
}: {
|
||||
currentTimezone: string;
|
||||
userId: string | undefined;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [value, setValue] = useState(currentTimezone);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(currentTimezone);
|
||||
}, [currentTimezone]);
|
||||
|
||||
const grouped = useMemo(() => {
|
||||
const groups: Record<string, typeof TIMEZONE_OPTIONS> = {};
|
||||
for (const tz of TIMEZONE_OPTIONS) {
|
||||
(groups[tz.group] ??= []).push(tz);
|
||||
}
|
||||
return groups;
|
||||
}, []);
|
||||
|
||||
const detected = useMemo(() => {
|
||||
try {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const nowInTz = useMemo(() => {
|
||||
try {
|
||||
return new Intl.DateTimeFormat("pt-BR", {
|
||||
timeZone: value,
|
||||
dateStyle: "short",
|
||||
timeStyle: "medium",
|
||||
}).format(new Date());
|
||||
} catch {
|
||||
return "—";
|
||||
}
|
||||
}, [value]);
|
||||
|
||||
const update = useMutation({
|
||||
mutationFn: async (tz: string) => {
|
||||
if (!userId) throw new Error("Sem sessão");
|
||||
const { error } = await supabase
|
||||
.from("profiles")
|
||||
.update({ timezone: tz })
|
||||
.eq("id", userId);
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Fuso horário atualizado.");
|
||||
queryClient.invalidateQueries({ queryKey: ["profile"] });
|
||||
},
|
||||
onError: (e: Error) => toast.error(translateAuthError(e.message)),
|
||||
});
|
||||
|
||||
const dirty = value !== currentTimezone;
|
||||
const isDetectedListed = detected
|
||||
? TIMEZONE_OPTIONS.some((t) => t.value === detected)
|
||||
: false;
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-border bg-card p-6 shadow-soft">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Globe className="h-5 w-5 text-primary" />
|
||||
<h2 className="text-lg font-semibold">Fuso horário</h2>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mb-6">
|
||||
Usado para agendar e exibir suas automações (cronjobs).
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="timezone">Selecione o fuso</Label>
|
||||
<Select value={value} onValueChange={setValue}>
|
||||
<SelectTrigger id="timezone" className="rounded-lg">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-80">
|
||||
{Object.entries(grouped).map(([group, items]) => (
|
||||
<SelectGroup key={group}>
|
||||
<SelectLabel>{group}</SelectLabel>
|
||||
{items.map((tz) => (
|
||||
<SelectItem key={tz.value} value={tz.value}>
|
||||
{tz.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectGroup>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<p>
|
||||
<span className="font-medium text-foreground">Agora neste fuso:</span> {nowInTz}
|
||||
</p>
|
||||
{detected && detected !== value && (
|
||||
<p>
|
||||
Seu navegador está em <span className="font-mono">{detected}</span>.
|
||||
{isDetectedListed && (
|
||||
<button
|
||||
type="button"
|
||||
className="ml-1 text-primary underline"
|
||||
onClick={() => setValue(detected)}
|
||||
>
|
||||
Usar este fuso
|
||||
</button>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() => update.mutate(value)}
|
||||
disabled={!dirty || update.isPending}
|
||||
className="rounded-lg bg-primary hover:bg-primary-dark text-primary-foreground transition-all duration-150 active:scale-[0.98]"
|
||||
>
|
||||
{update.isPending && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
Salvar fuso horário
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue