diff --git a/frontend/src/app/(public)/servicos/page.tsx b/frontend/src/app/(public)/servicos/page.tsx index 6019714..7434a90 100644 --- a/frontend/src/app/(public)/servicos/page.tsx +++ b/frontend/src/app/(public)/servicos/page.tsx @@ -1,35 +1,68 @@ "use client"; +import { useState, useEffect } from "react"; import Link from "next/link"; import { T } from "@/components/TranslatedText"; +type Service = { + id: string; + title: string; + icon: string; + shortDescription: string | null; + fullDescription: string | null; + active: boolean; + order: number; +}; + +const FALLBACK_SERVICES = [ + { + icon: "ri-draft-line", + title: "Projetos Técnicos", + shortDescription: "Desenvolvimento de projetos de engenharia mecânica, estrutural e veicular com alta precisão e conformidade normativa.", + }, + { + icon: "ri-truck-line", + title: "Engenharia Veicular", + shortDescription: "Expertise em modificações, adaptações e homologações veiculares com foco em segurança e conformidade.", + }, + { + icon: "ri-file-paper-2-line", + title: "Laudos e Perícias", + shortDescription: "Emissão de laudos técnicos e pareceres periciais para equipamentos, estruturas e veículos.", + }, + { + icon: "ri-tools-fill", + title: "Consultoria Técnica", + shortDescription: "Assessoria especializada para adequação de frotas, planos de Rigging e supervisão de manutenção de equipamentos de carga.", + } +]; + export default function ServicosPage() { - const services = [ - { - icon: "ri-draft-line", - title: "Projetos Técnicos", - description: "Desenvolvimento de projetos de engenharia mecânica, estrutural e veicular com alta precisão e conformidade normativa.", - features: ["Projeto Mecânico 3D", "Cálculo Estrutural", "Dispositivos Especiais", "Homologação de Equipamentos"] - }, - { - icon: "ri-truck-line", - title: "Engenharia Veicular", - description: "Expertise em modificações, adaptações e homologações veiculares com foco em segurança e conformidade.", - features: ["Projeto de Instalação", "Estudo de Estabilidade", "Adequação de Carrocerias", "Regularização Veicular"] - }, - { - icon: "ri-file-paper-2-line", - title: "Laudos e Perícias", - description: "Emissão de laudos técnicos e pareceres periciais para equipamentos, estruturas e veículos.", - features: ["Laudos de Munck/Guindaste", "Inspeção de Segurança", "Teste de Carga", "Certificação de Equipamentos"] - }, - { - icon: "ri-tools-fill", - title: "Consultoria Técnica", - description: "Assessoria especializada para adequação de frotas, planos de Rigging e supervisão de manutenção de equipamentos de carga.", - features: ["Plano de Rigging", "Supervisão de Manutenção", "Consultoria em Normas", "Treinamento Operacional"] + const [services, setServices] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchServices() { + try { + const res = await fetch('/api/services'); + if (res.ok) { + const data = await res.json(); + // Filtrar apenas serviços ativos e ordenar + const activeServices = data + .filter((s: Service) => s.active) + .sort((a: Service, b: Service) => a.order - b.order); + setServices(activeServices); + } + } catch (error) { + console.error('Erro ao carregar serviços:', error); + } finally { + setLoading(false); + } } - ]; + fetchServices(); + }, []); + + const displayServices = services.length > 0 ? services : FALLBACK_SERVICES; return (
@@ -48,44 +81,37 @@ export default function ServicosPage() { {/* Services List */}
-
- {services.map((service, index) => ( -
-
- -
- -
-
-
- -
- 0{index + 1} + {loading ? ( +
+ +
+ ) : ( +
+ {displayServices.map((service, index) => ( +
+
+
-

{service.title}

-

- {service.description} -

+
+
+
+ +
+ 0{index + 1} +
+ +

+ {service.title} +

+

+ {service.shortDescription || ''} +

+
- -
-

- - Escopo de Atuação -

-
    - {service.features.map((feature, idx) => ( -
  • - - {feature} -
  • - ))} -
-
-
- ))} -
+ ))} +
+ )}
diff --git a/frontend/src/app/[locale]/servicos/page.tsx b/frontend/src/app/[locale]/servicos/page.tsx index bfe3499..702af7b 100644 --- a/frontend/src/app/[locale]/servicos/page.tsx +++ b/frontend/src/app/[locale]/servicos/page.tsx @@ -1,59 +1,72 @@ "use client"; +import { useState, useEffect } from "react"; import Link from "next/link"; import { useLocale } from "@/contexts/LocaleContext"; +type Service = { + id: string; + title: string; + icon: string; + shortDescription: string | null; + fullDescription: string | null; + active: boolean; + order: number; +}; + export default function ServicosPage() { const { t, locale } = useLocale(); const prefix = locale === 'pt' ? '' : `/${locale}`; + const [services, setServices] = useState([]); + const [loading, setLoading] = useState(true); - const services = [ + // Fallback services usando traduções + const fallbackServices = [ { icon: "ri-draft-line", title: t('services.technical.title'), - description: t('services.technical.description'), - features: [ - t('services.technical.feature1'), - t('services.technical.feature2'), - t('services.technical.feature3'), - t('services.technical.feature4') - ] + shortDescription: t('services.technical.description'), }, { icon: "ri-truck-line", title: t('services.vehicular.title'), - description: t('services.vehicular.description'), - features: [ - t('services.vehicular.feature1'), - t('services.vehicular.feature2'), - t('services.vehicular.feature3'), - t('services.vehicular.feature4') - ] + shortDescription: t('services.vehicular.description'), }, { icon: "ri-file-paper-2-line", title: t('services.reports.title'), - description: t('services.reports.description'), - features: [ - t('services.reports.feature1'), - t('services.reports.feature2'), - t('services.reports.feature3'), - t('services.reports.feature4') - ] + shortDescription: t('services.reports.description'), }, { icon: "ri-tools-fill", title: t('services.consulting.title'), - description: t('services.consulting.description'), - features: [ - t('services.consulting.feature1'), - t('services.consulting.feature2'), - t('services.consulting.feature3'), - t('services.consulting.feature4') - ] + shortDescription: t('services.consulting.description'), } ]; + useEffect(() => { + async function fetchServices() { + try { + const res = await fetch('/api/services'); + if (res.ok) { + const data = await res.json(); + // Filtrar apenas serviços ativos e ordenar + const activeServices = data + .filter((s: Service) => s.active) + .sort((a: Service, b: Service) => a.order - b.order); + setServices(activeServices); + } + } catch (error) { + console.error('Erro ao carregar serviços:', error); + } finally { + setLoading(false); + } + } + fetchServices(); + }, []); + + const displayServices = services.length > 0 ? services : fallbackServices; + return (
{/* Hero Section */} @@ -71,44 +84,37 @@ export default function ServicosPage() { {/* Services List */}
-
- {services.map((service, index) => ( -
-
- -
- -
-
-
- -
- 0{index + 1} + {loading ? ( +
+ +
+ ) : ( +
+ {displayServices.map((service, index) => ( +
+
+
-

{service.title}

-

- {service.description} -

+
+
+
+ +
+ 0{index + 1} +
+ +

+ {service.title} +

+

+ {service.shortDescription || ''} +

+
- -
-

- - {t('services.scope')} -

-
    - {service.features.map((feature, idx) => ( -
  • - - {feature} -
  • - ))} -
-
-
- ))} -
+ ))} +
+ )}
diff --git a/frontend/src/app/admin/servicos/[id]/editar/page.tsx b/frontend/src/app/admin/servicos/[id]/editar/page.tsx new file mode 100644 index 0000000..5269c11 --- /dev/null +++ b/frontend/src/app/admin/servicos/[id]/editar/page.tsx @@ -0,0 +1,273 @@ +"use client"; + +import { useState, useEffect, use } from 'react'; +import Link from 'next/link'; +import { useRouter } from 'next/navigation'; +import { useToast } from '@/contexts/ToastContext'; + +// Ícones disponíveis do Remix Icon +const ICON_OPTIONS = [ + { value: 'ri-settings-3-line', label: 'Configurações' }, + { value: 'ri-tools-line', label: 'Ferramentas' }, + { value: 'ri-truck-line', label: 'Caminhão' }, + { value: 'ri-bus-line', label: 'Ônibus' }, + { value: 'ri-car-line', label: 'Carro' }, + { value: 'ri-roadster-line', label: 'Veículo Esportivo' }, + { value: 'ri-shield-check-line', label: 'Segurança' }, + { value: 'ri-file-chart-line', label: 'Laudo/Relatório' }, + { value: 'ri-draft-line', label: 'Documento' }, + { value: 'ri-building-2-line', label: 'Empresa' }, + { value: 'ri-home-gear-line', label: 'Manutenção' }, + { value: 'ri-compass-3-line', label: 'Engenharia' }, + { value: 'ri-ruler-line', label: 'Medição' }, + { value: 'ri-flashlight-line', label: 'Inspeção' }, + { value: 'ri-hard-drive-2-line', label: 'Equipamento' }, + { value: 'ri-cpu-line', label: 'Tecnologia' }, +]; + +export default function EditService({ params }: { params: Promise<{ id: string }> }) { + const resolvedParams = use(params); + const router = useRouter(); + const { success, error } = useToast(); + const [loadingData, setLoadingData] = useState(true); + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + title: '', + icon: 'ri-settings-3-line', + active: true, + order: 0, + shortDescription: '', + fullDescription: '' + }); + + // Carregar dados do serviço + useEffect(() => { + async function fetchService() { + try { + const res = await fetch(`/api/services/${resolvedParams.id}`); + if (!res.ok) { + throw new Error('Serviço não encontrado'); + } + const service = await res.json(); + + setFormData({ + title: service.title || '', + icon: service.icon || 'ri-settings-3-line', + active: service.active ?? true, + order: service.order ?? 0, + shortDescription: service.shortDescription || '', + fullDescription: service.fullDescription || '', + }); + } catch (err) { + console.error('Erro ao carregar serviço:', err); + error('Não foi possível carregar o serviço.'); + router.push('/admin/servicos'); + } finally { + setLoadingData(false); + } + } + fetchService(); + }, [resolvedParams.id, error, router]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!formData.title) { + error('Informe ao menos o título do serviço.'); + return; + } + + setLoading(true); + try { + const response = await fetch(`/api/services/${resolvedParams.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: formData.title.trim(), + icon: formData.icon.trim() || 'ri-settings-3-line', + shortDescription: formData.shortDescription?.trim() || null, + fullDescription: formData.fullDescription?.trim() || null, + active: formData.active, + order: formData.order, + }), + }); + + const data = await response.json().catch(() => ({})); + + if (!response.ok) { + throw new Error(data?.error || 'Erro ao atualizar serviço'); + } + + success('Serviço atualizado com sucesso!'); + router.push('/admin/servicos'); + } catch (err) { + console.error('Erro ao atualizar serviço:', err); + error(err instanceof Error ? err.message : 'Não foi possível atualizar o serviço.'); + } finally { + setLoading(false); + } + }; + + if (loadingData) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+
+ + + +
+

Editar Serviço

+

Atualize as informações do serviço.

+
+
+ +
+ {/* Informações Básicas */} +
+

+ + Informações Básicas +

+ +
+
+ + setFormData({...formData, title: e.target.value})} + className="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" + placeholder="Ex: Engenharia Veicular" + required + /> +
+ +
+ +
+
+ +
+ +
+

Ou digite manualmente uma classe do Remix Icon

+ setFormData({...formData, icon: e.target.value})} + className="mt-2 w-full px-4 py-2 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white text-sm focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" + placeholder="ri-icon-name" + /> +
+ +
+
+ + +
+ +
+ + setFormData({...formData, order: parseInt(e.target.value) || 0})} + className="w-full px-4 py-3 bg-gray-50 dark:bg-white/5 border border-gray-200 dark:border-white/10 rounded-xl text-gray-900 dark:text-white focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary transition-all" + min="0" + /> +

Menor número = aparece primeiro

+
+
+
+
+ + {/* Descrições */} +
+

+ + Descrições +

+ +
+
+ +