Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/api/appointments.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,10 @@ def get_appointments():
raise ValueError('Invalid date range.')
except ValueError as error:
return jsonify(error=str(error)), 400
query = select(Appointment).where(Appointment.company_id == g.company_id)
query = select(Appointment).where(
Appointment.company_id == g.company_id,
Appointment.status != AppointmentStatus.CANCELLED,
)
if start:
query = query.where(Appointment.ends_at > start)
if end:
Expand Down
19 changes: 19 additions & 0 deletions src/front/i18n/literalTranslations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const literalTranslations = [
phrase("Not specified", "No especificado", "Não especificado"),
phrase("No priority", "Sin prioridad", "Sem prioridade"),
phrase("No date", "Sin fecha", "Sem data"),
phrase("To be defined", "Por definir", "A definir"),
phrase("Try again", "Reintentar", "Tentar novamente"),
phrase("Company", "Empresa", "Empresa"),
phrase("Client", "Cliente", "Cliente"),
phrase("Member", "Miembro", "Membro"),
Expand Down Expand Up @@ -90,6 +92,7 @@ export const literalTranslations = [
phrase("Do you want to {action} this client?", "¿Quieres {action} este cliente?", "Deseja {action} este cliente?"),
phrase("No clients found.", "No se encontraron clientes.", "Nenhum cliente encontrado."),
phrase("Client management", "Gestión de Clientes", "Gestão de clientes"),
phrase("Customer directory, addresses and commercial activity.", "Directorio de clientes, direcciones y actividad comercial.", "Diretório de clientes, endereços e atividade comercial."),
phrase("New client", "Nuevo cliente", "Novo cliente"),
phrase("View details", "Ver detalles", "Ver detalhes"),
phrase("Deactivate", "Desactivar", "Desativar"),
Expand All @@ -105,6 +108,14 @@ export const literalTranslations = [
phrase("Unable to save the client.", "No se pudo guardar el cliente.", "Não foi possível salvar o cliente."),
phrase("Unable to update the client status.", "No se pudo actualizar el estado del cliente.", "Não foi possível atualizar o status do cliente."),
phrase("Main address", "Dirección principal", "Endereço principal"),
phrase("Contact details and primary address.", "Datos de contacto y dirección principal.", "Dados de contato e endereço principal."),
phrase("Address", "Dirección", "Endereço"),
phrase("Label", "Etiqueta", "Etiqueta"),
phrase("City", "Ciudad", "Cidade"),
phrase("Postcode", "Código postal", "Código postal"),
phrase("Notes", "Notas", "Observações"),
phrase("Apartment, door or another reference", "Piso, puerta u otra referencia", "Apartamento, porta ou outra referência"),
phrase("The address is optional. If you start filling it in, complete the required fields.", "Es opcional. Si empiezas a rellenarla, completa los campos obligatorios.", "O endereço é opcional. Se começar a preenchê-lo, complete os campos obrigatórios."),
phrase("Preferences, context or useful client information", "Preferencias, contexto o información útil del cliente", "Preferências, contexto ou informações úteis do cliente"),
phrase("Loading address", "Cargando dirección", "Carregando endereço"),
phrase("Pagination", "Paginación", "Paginação"),
Expand Down Expand Up @@ -144,8 +155,10 @@ export const literalTranslations = [
phrase("Operational control of jobs, stages, materials and deliverables.", "Control operativo de encargos, etapas, materiales y entregables.", "Controle operacional de trabalhos, etapas, materiais e entregas."),
phrase("New job", "Nuevo Trabajo", "Novo trabalho"),
phrase("Pending", "Pendientes", "Pendentes"),
phrase("Pending", "Pendiente", "Pendente"),
phrase("In progress", "En curso", "Em andamento"),
phrase("Completed", "Completados", "Concluídos"),
phrase("Completed", "Completado", "Concluído"),
phrase("No jobs found", "No se encontraron trabajos", "Nenhum trabalho encontrado"),
phrase("Adjust the search filters or create a new job.", "Intenta ajustar los filtros de búsqueda o crea un nuevo trabajo.", "Ajuste os filtros de pesquisa ou crie um novo trabalho."),
phrase("Stage progress", "Progreso de etapas", "Progresso das etapas"),
Expand All @@ -165,6 +178,7 @@ export const literalTranslations = [
phrase("Network error while deleting the job.", "Error de red al intentar eliminar el trabajo.", "Erro de rede ao excluir o trabalho."),
phrase("General client", "Cliente general", "Cliente geral"),
phrase("Address not specified", "Dirección no especificada", "Endereço não especificado"),
phrase("Delivery:", "Entrega:", "Entrega:"),
phrase("E.g. Built-in wardrobe", "Ej. Fabricación de Armario Empotrado", "Ex.: Fabricação de armário embutido"),
phrase("Error loading data:", "Error cargando datos:", "Erro ao carregar dados:"),
phrase("Error creating the job:", "Error al crear el trabajo:", "Erro ao criar o trabalho:"),
Expand Down Expand Up @@ -255,6 +269,11 @@ export const literalTranslations = [
phrase("Take control before sending a message.", "Asume el control antes de enviar un mensaje.", "Assuma o controle antes de enviar uma mensagem."),
phrase("Conversation not found.", "No se encontró la conversación.", "Conversa não encontrada."),
phrase("Your subscription is inactive or expired.", "Tu suscripción está inactiva o ha caducado.", "Sua assinatura está inativa ou expirou."),
phrase("Manage and convert new opportunities.", "Gestiona y convierte nuevas oportunidades.", "Gerencie e converta novas oportunidades."),
phrase("Create new", "Crear nuevo", "Criar novo"),
phrase("Clear", "Limpiar", "Limpar"),
phrase("Status", "Estado", "Status"),
phrase("The contact authorized the processing of their data.", "El contacto autorizó el tratamiento de sus datos.", "O contato autorizou o tratamento dos seus dados."),
phrase("Client not found.", "No se encontró el cliente.", "Cliente não encontrado."),
phrase("The initial message cannot be empty.", "El mensaje inicial no puede estar vacío.", "A mensagem inicial não pode estar vazia."),
phrase("Unable to connect to the server.", "No se pudo conectar con el servidor.", "Não foi possível conectar ao servidor."),
Expand Down
5 changes: 4 additions & 1 deletion src/front/pages/Agenda.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,10 @@ export const Agenda = () => {
const handleDeleteAppointment = async (id) => {
if (busy || !window.confirm(ui.confirmCancelAppointment)) return;
setBusy(true);
try { await cancelAppointment(options, id); setRevision(value => value + 1); }
try {
await cancelAppointment(options, id);
setAppointments(current => current.filter(item => item.id !== id));
}
catch (error) { setErrorMsg(error.message || ui.appointmentCancelError); }
finally { setBusy(false); }
};
Expand Down
49 changes: 24 additions & 25 deletions src/front/pages/Agents.css
Original file line number Diff line number Diff line change
@@ -1,58 +1,57 @@
/* Scoped light appearance matches the existing Inbox workspace. */
/* Scoped workspace appearance follows the shared light/dark theme. */
.agents-page {
color-scheme: light;
color: #252b40;
background: #f7f7fc;
color: var(--cf-text);
background: var(--cf-workspace-bg);
min-height: calc(100vh - 48px);
padding: 32px;
--bs-body-color: #252b40;
--bs-body-bg: #ffffff;
--bs-border-color: #e5e5f0;
--bs-body-color: var(--cf-text);
--bs-body-bg: var(--cf-surface);
--bs-border-color: var(--cf-border);
}
.agents-page > h1 { font-size: 32px; font-weight: 650; letter-spacing: -1px; margin: 0 0 12px; color: #252b40; }
.agents-page > p { color: #647087; max-width: 760px; line-height: 1.7; margin-bottom: 32px; }
.agents-page h2 { font-size: 19px; font-weight: 650; color: #252b40; margin: 0 0 24px; }
.agents-page > h1 { font-size: 32px; font-weight: 650; letter-spacing: -1px; margin: 0 0 12px; color: var(--cf-text); }
.agents-page > p { color: var(--cf-text-muted); max-width: 760px; line-height: 1.7; margin-bottom: 32px; }
.agents-page h2 { font-size: 19px; font-weight: 650; color: var(--cf-text); margin: 0 0 24px; }
.agents-page .row { align-items: flex-start; }
.agents-page form {
background: #fff;
border: 1px solid #e5e5f0;
background: var(--cf-surface);
border: 1px solid var(--cf-border);
border-radius: 18px;
padding: 28px;
box-shadow: 0 8px 30px #252b4005;
}
.agents-page .card {
background: #fff;
color: #252b40;
border: 1px solid #e5e5f0;
background: var(--cf-surface);
color: var(--cf-text);
border: 1px solid var(--cf-border);
border-radius: 16px;
padding: 24px !important;
gap: 10px;
border-left: 4px solid #7859df;
}
.agents-page .card strong { font-size: 17px; overflow-wrap: anywhere; }
.agents-page .card p { color: #647087; font-size: 14px; margin-bottom: 8px; }
.agents-page label { font-size: 14px; font-weight: 550; color: #414b61; }
.agents-page .card p { color: var(--cf-text-muted); font-size: 14px; margin-bottom: 8px; }
.agents-page label { font-size: 14px; font-weight: 550; color: var(--cf-text); }
.agents-page form > label { margin-bottom: 20px !important; }
.agents-page .form-control {
background: #fff;
color: #252b40;
border: 1px solid #d9deea;
background: var(--cf-surface-muted);
color: var(--cf-text);
border: 1px solid var(--cf-border);
border-radius: 10px;
padding: 12px 14px;
margin-top: 8px;
line-height: 1.6;
}
.agents-page .form-control:focus { background: #fff; color: #252b40; border-color: #7859df; box-shadow: 0 0 0 3px #7859df22; }
.agents-page .form-control:focus { background: var(--cf-surface-muted); color: var(--cf-text); border-color: #7859df; box-shadow: 0 0 0 3px #7859df22; }
.agents-page textarea { min-height: 180px; resize: vertical; }
.agents-page fieldset { background: #f8f7fd; border: 1px solid #e8e4f5; border-radius: 12px; padding: 18px; margin-top: 8px; }
.agents-page legend { float: none; width: auto; font-size: 15px; font-weight: 650; margin-bottom: 10px; color: #414b61; }
.agents-page fieldset { background: var(--cf-surface-muted); border: 1px solid var(--cf-border); border-radius: 12px; padding: 18px; margin-top: 8px; }
.agents-page legend { float: none; width: auto; font-size: 15px; font-weight: 650; margin-bottom: 10px; color: var(--cf-text); }
.agents-page fieldset label { display: flex !important; gap: 10px; align-items: flex-start; padding: 8px 0; overflow-wrap: anywhere; }
.agents-page input[type="checkbox"] { width: 18px; height: 18px; margin-top: 2px; flex-shrink: 0; accent-color: #7357d9; }
.agents-page fieldset p { color: #647087; font-size: 14px; margin: 0; }
.agents-page fieldset p { color: var(--cf-text-muted); font-size: 14px; margin: 0; }
.agents-page .btn { border-radius: 10px; padding: 11px 18px; font-size: 14px; font-weight: 600; }
.agents-page .btn-primary { background: #7357d9; border-color: #7357d9; color: #fff; }
.agents-page .btn-primary:hover { background: #6045bf; border-color: #6045bf; }
.agents-page .btn-outline-primary { color: #6548c5; border-color: #d7cdef; background: #fff; }
.agents-page .btn-outline-primary { color: #8b73e8; border-color: #7864bd; background: var(--cf-surface); }
.agents-page .btn-outline-primary:hover { color: #5135ae; background: #f0ebfc; }
.agents-page button:focus-visible { outline: 3px solid #aa94ed; outline-offset: 3px; }
.agents-page .btn:disabled { opacity: .55; }
Expand Down
1 change: 0 additions & 1 deletion src/front/pages/ClientDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ export const ClientDetail = () => {
.toUpperCase() || "?"}
</span>
<div>
<span className="text-muted small fw-semibold">ID: {client.id}</span>
<h2 className="fw-bold text-dark mb-1">{fullName}</h2>
<h5 className="text-secondary fs-6 mb-2">
Cliente
Expand Down
2 changes: 1 addition & 1 deletion src/front/pages/Clients.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ export const Clients = () => {
{fullName}
</h5>
<span className="text-secondary small">
{translateLiteral("Cliente", locale)} #{client.id}
{translateLiteral("Cliente", locale)}
</span>
</div>
</div>
Expand Down
61 changes: 61 additions & 0 deletions src/front/pages/Inbox.css
Original file line number Diff line number Diff line change
Expand Up @@ -466,3 +466,64 @@
grid-template-columns: 1fr;
}
}

:root[data-bs-theme="dark"] .inbox-page {
color: var(--cf-text);
background: var(--cf-workspace-bg);
}
:root[data-bs-theme="dark"] .inbox-heading h1,
:root[data-bs-theme="dark"] .inbox-conversation,
:root[data-bs-theme="dark"] .inbox-details h2,
:root[data-bs-theme="dark"] .inbox-empty-chat h2,
:root[data-bs-theme="dark"] .inbox-modal,
:root[data-bs-theme="dark"] .inbox-modal label {
color: var(--cf-text);
}
:root[data-bs-theme="dark"] .inbox-heading p,
:root[data-bs-theme="dark"] .inbox-conversation-text small,
:root[data-bs-theme="dark"] .inbox-chat-heading small,
:root[data-bs-theme="dark"] .inbox-details > p,
:root[data-bs-theme="dark"] .inbox-details dt,
:root[data-bs-theme="dark"] .inbox-empty-list p,
:root[data-bs-theme="dark"] .inbox-empty-chat p,
:root[data-bs-theme="dark"] .inbox-note,
:root[data-bs-theme="dark"] .inbox-modal label span {
color: var(--cf-text-muted);
}
:root[data-bs-theme="dark"] .inbox-workspace,
:root[data-bs-theme="dark"] .inbox-chat-heading,
:root[data-bs-theme="dark"] .inbox-message,
:root[data-bs-theme="dark"] .inbox-composer,
:root[data-bs-theme="dark"] .inbox-client-select,
:root[data-bs-theme="dark"] .inbox-pagination button,
:root[data-bs-theme="dark"] .inbox-control button,
:root[data-bs-theme="dark"] .inbox-modal {
color: var(--cf-text);
background: var(--cf-surface);
border-color: var(--cf-border);
}
:root[data-bs-theme="dark"] .inbox-chat,
:root[data-bs-theme="dark"] .inbox-secondary-action,
:root[data-bs-theme="dark"] .inbox-composer textarea,
:root[data-bs-theme="dark"] .inbox-modal input,
:root[data-bs-theme="dark"] .inbox-modal select,
:root[data-bs-theme="dark"] .inbox-modal textarea {
color: var(--cf-text);
background: var(--cf-surface-muted);
border-color: var(--cf-border);
}
:root[data-bs-theme="dark"] .inbox-conversations,
:root[data-bs-theme="dark"] .inbox-chat-heading,
:root[data-bs-theme="dark"] .inbox-composer,
:root[data-bs-theme="dark"] .inbox-details,
:root[data-bs-theme="dark"] .inbox-modal > header {
border-color: var(--cf-border);
}
:root[data-bs-theme="dark"] .inbox-conversation:hover,
:root[data-bs-theme="dark"] .inbox-conversation.is-selected {
background: rgba(120, 89, 223, 0.2);
}
:root[data-bs-theme="dark"] .inbox-error {
color: #fecdd3;
background: rgba(159, 18, 57, 0.28);
}
6 changes: 3 additions & 3 deletions src/front/pages/JobDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const JobDetail = () => {
const [job, setJob] = useState(location.state?.job || null);
const [loading, setLoading] = useState(!location.state?.job);
const [error, setError] = useState("");
const displayDate = (value) => value ? String(value).slice(0, 10) : translateLiteral("Por definir", locale);

// Helper para obtener la URL base limpia y el token de autenticación
const getBaseUrl = () => (import.meta.env.VITE_BACKEND_URL || "").replace(/\/$/, "");
Expand Down Expand Up @@ -176,7 +177,6 @@ export const JobDetail = () => {
<div className="card-body p-4">
<div className="row g-4 align-items-center">
<div className="col-12 col-lg-7">
<span className="text-muted small fw-semibold">ID: {job.id}</span>
<h2 className="fw-bold text-dark mb-2">{job.title}</h2>
<p className="text-secondary mb-3">
<i className="fa-solid fa-user text-primary me-2"></i>
Expand All @@ -196,11 +196,11 @@ export const JobDetail = () => {
</div>
<div className="d-flex justify-content-between mb-2">
<span className="text-secondary small">Fecha de inicio:</span>
<span className="fw-semibold text-dark small">{job.startDate || job.start_date || "Por definir"}</span>
<span className="fw-semibold text-dark small">{displayDate(job.scheduled_start)}</span>
</div>
<div className="d-flex justify-content-between mb-2">
<span className="text-secondary small">Fecha de entrega:</span>
<span className="fw-semibold text-dark small">{job.dueDate || job.due_date || "Por definir"}</span>
<span className="fw-semibold text-dark small">{displayDate(job.scheduled_end)}</span>
</div>
<div className="d-flex justify-content-between">
<span className="text-secondary small">Equipo asignado:</span>
Expand Down
11 changes: 5 additions & 6 deletions src/front/pages/Jobs.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { addDays, defaultJobSchedule } from "../utils/jobSchedule.mjs";
import { addDays, defaultJobSchedule, jobDate, jobMatchesStatus } from "../utils/jobSchedule.mjs";
import { useLanguage } from "../context/LanguageContext";
import { translateLiteral } from "../i18n/literalTranslations.mjs";

Expand Down Expand Up @@ -140,7 +140,7 @@ export const Jobs = () => {

const matchesSearch = jobTitle.toLowerCase().includes(searchTerm.toLowerCase()) ||
clientName.toLowerCase().includes(searchTerm.toLowerCase());
const matchesStatus = statusFilter === "all" || job.status === statusFilter;
const matchesStatus = jobMatchesStatus(job.status, statusFilter);
return matchesSearch && matchesStatus;
});

Expand Down Expand Up @@ -175,8 +175,8 @@ export const Jobs = () => {
client_id: newJob.client_id ? Number(newJob.client_id) : null,
quoted_amount: Number(newJob.budget),
description: `Obra en ${newJob.address}`,
scheduled_start: `${newJob.startDate}T00:00:00`,
scheduled_end: `${newJob.dueDate}T23:59:59`
scheduled_start: newJob.startDate,
scheduled_end: newJob.dueDate
})
});

Expand Down Expand Up @@ -295,7 +295,6 @@ export const Jobs = () => {
<div>
<div className="d-flex justify-content-between align-items-start mb-2">
<div>
<span className="text-muted" style={{ fontSize: "0.75rem" }}>ID: {job.id}</span>
<h5 className="fw-bold text-dark mb-1">{job.title}</h5>
</div>
{getStatusBadge(job.status)}
Expand Down Expand Up @@ -331,7 +330,7 @@ export const Jobs = () => {
</div>
<div>
<i className="fa-solid fa-calendar me-1"></i>
<span className="text-dark">Entrega: {job.scheduled_end ? job.scheduled_end.slice(0, 10) : "Por definir"}</span>
<span className="text-dark">Entrega: {jobDate(job.scheduled_end) || "Por definir"}</span>
</div>
</div>
</div>
Expand Down
Loading
Loading