first commit

This commit is contained in:
2026-07-16 20:48:43 -03:00
commit 5696cee264
1111 changed files with 322270 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
export interface Frame {
id: number;
title: string;
description: string;
image: string;
buttonText: string;
showCategoryId: number;
}
export const sliderFrames: Frame[] = [
{
id: 1,
title: "Aprende a tu ritmo con clases particulares",
description:
"Reserva tu turno con profesores especializados que te ayudarán a superar desafíos escolares y alcanzar tus metas académicas. Elige el horario que mejor se adapte a ti y mejora tu rendimiento desde la comodidad de tu hogar o en un entorno personalizado",
image: "/categories/particular.png",
buttonText: "Explorar categoria",
showCategoryId: 200,
},
{
id: 2,
title: "Cuida tu bienestar emocional",
description:
"Reserva tu turno con psicólogos profesionales listos para escucharte y acompañarte en cada paso. Elige el horario que se adapte a tus necesidades y recibe apoyo en un ambiente seguro y confidencial.",
image: "/categories/psico.png",
buttonText: "Explorar esta categoria",
showCategoryId: 100,
},
{
id: 3,
title: "Tu sonrisa en las mejores manos",
description:
"Agenda tu turno con dentistas calificados y mantén tu salud bucal en óptimas condiciones. Selecciona el horario que prefieras y disfruta de una atención profesional en un ambiente cómodo y seguro.",
image: "/categories/dentista.png",
buttonText: "Explorar esta categoria",
showCategoryId: 100,
},
];
+23
View File
@@ -0,0 +1,23 @@
"use client";
import { useEffect } from "react";
import { useNavigation } from "../hooks/goto";
import { useParams } from "next/navigation";
type Params = {
slug: string;
};
export default function SlugRedirector() {
const { slug } = useParams<Params>();
const { goTo } = useNavigation();
useEffect(() => {
if (slug == "sitemap.xml") {
goTo("/sitemap.xml");
return;
}
goTo("/landing/org/" + slug);
}, []);
return <></>;
}
@@ -0,0 +1,210 @@
import ApiRequest from "@services/Api.Service";
import {
ClientMergeParams,
DeleteClientParams,
FindClientByIdParams,
OrganizationClientView,
UpdateClientParams,
} from "@models/Clients.model";
import { ApiVoidResult } from "@core/Models/Api.VoidResult.type";
import * as Yup from "yup";
import {
CreateRepeatsParams,
DeleteRepeatsParams,
FindRepeatsParams,
IRepeat,
RepeatView,
GetPendingRepeatsParams,
PendingRepeatView,
} from "@core/Models/Repeats.model";
import {
ClientAccountReport,
ClientAccountView,
FindClientAccountsParams,
GetClientAccountReportParams,
} from "@core/Models/ClientAccounts.model";
import {
CreateClientAccountMovementParams,
IClientAccountMovement,
PaginateClientAccountMovementsParams,
PaginateClientAccountMovementsResults,
} from "@core/Models/ClientAccountMovements.model";
import { ApiError } from "@core/Models/Server.Error.model";
const schemaRepeatsAdd = Yup.object().shape({
companyId: Yup.string().required("Debe seleccionar una organización."),
employeeId: Yup.string().required("Debe seleccionar un profesional."),
serviceId: Yup.string().required("Debe seleccionar un servicio."),
clientId: Yup.string().required("Debe seleccionar un cliente."),
weekDay: Yup.number().required("Debe indicar un día de la semana."),
startHour: Yup.number().required("Debe indicar una hora de inicio."),
sessionUser: Yup.string().required("Para crear una organización debe iniciar sesión."),
});
const schemaClientUpdate = Yup.object().shape({
firstName: Yup.string().required("Debe introducir el nombre del cliente."),
lastName: Yup.string().required("Debe introducir el apellido del cliente."),
email: Yup.string().required().email("Debe introducir un E-mail valido."),
phoneAreaCode: Yup.number()
.required("Debe introducir el código de área.")
.typeError("El código de área debe ser un número."),
phoneNumber: Yup.number()
.required("Debe introducir el número de teléfono.")
.typeError("El número de teléfono debe ser un número."),
sessionUser: Yup.string().required("Para crear una organización debe iniciar sesión."),
});
const schemaClientMerge = Yup.object().shape({
fromClientId: Yup.string().required(
"Para fusionar dos clientes es necesario seleccionar el cliente de orgigen."
),
toClientId: Yup.string().required(
"Para fusionar dos clientes es necesario seleccionar el cliente de destino."
),
sessionUser: Yup.string().required("Para crear una organización debe iniciar sesión."),
});
const schemaAccountMovementAdd = Yup.object().shape({
clientId: Yup.string().required("Debe seleccionar un cliente."),
description: Yup.string().required("Debe introducir una descripción para el movimiento."),
paymentMethod: Yup.string().required("Debe seleccionar un método de pago."),
type: Yup.string().required("Debe seleccionar un tipo de movimiento."),
status: Yup.string().required("Debe seleccionar un estado del movimiento."),
amount: Yup.number()
.required("Debe introducir el monto del movimiento.")
.typeError("El monto debe ser un número.")
.positive("El monto debe ser un número positivo."),
sessionUser: Yup.string().required("Para cargar un movimiento debe iniciar sesión."),
});
export const findClientById = async (
data: FindClientByIdParams
): Promise<OrganizationClientView> => {
return ApiRequest.post<OrganizationClientView>("clients/find-by-id", data);
};
export const updateClient = async (data: UpdateClientParams): Promise<ApiVoidResult> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaClientUpdate.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new Error(yupErrors.message);
}
return ApiRequest.post<ApiVoidResult>("clients/update", data);
};
export const deleteClient = async (data: DeleteClientParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("clients/delete", data);
};
export const clientMerge = async (data: ClientMergeParams): Promise<ApiVoidResult> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaClientMerge.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new Error(yupErrors.message);
}
return ApiRequest.post<ApiVoidResult>("clients/merge", data);
};
export const repeatAdd = async (data: CreateRepeatsParams): Promise<IRepeat> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaRepeatsAdd.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new Yup.ValidationError(yupErrors.message);
}
return ApiRequest.post<IRepeat>("repeats/create", data);
};
export const getRepeatsByClient = async (data: FindRepeatsParams): Promise<RepeatView[]> => {
return ApiRequest.post<RepeatView[]>("repeats/repeats-by-client", data);
};
export const deleteRepeat = async (data: DeleteRepeatsParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("repeats/delete", data);
};
export const getPendingRepeatsCurrentWeek = async (data: GetPendingRepeatsParams): Promise<PendingRepeatView[]> => {
return ApiRequest.post<PendingRepeatView[]>("repeats/pending-current-week", data);
};
export const getClientAccountView = async (
data: FindClientAccountsParams
): Promise<ClientAccountView> => {
return ApiRequest.post<ClientAccountView>("client-accounts/client-view", data);
};
export const addClientAccountMovement = async (
data: CreateClientAccountMovementParams
): Promise<IClientAccountMovement> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaAccountMovementAdd.validate(data, {
abortEarly: true,
});
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new ApiError(400, yupErrors.message);
}
return ApiRequest.post<IClientAccountMovement>("client-accounts/add-movement", data);
};
export const paginateClientAccountMovements = async (
data: PaginateClientAccountMovementsParams
): Promise<PaginateClientAccountMovementsResults> => {
return ApiRequest.post<PaginateClientAccountMovementsResults>(
"client-accounts/paginate-movements",
data
);
};
export const GetClientAccountReport = async (
data: GetClientAccountReportParams
): Promise<ClientAccountReport> => {
return ApiRequest.post<ClientAccountReport>("client-accounts/get-account-report", data);
};
@@ -0,0 +1,75 @@
@media (max-width: 851px) {
.clientProfileHeader {
position: absolute;
top: -70px;
margin-left: 30px;
display: flex;
}
.clientProfileHeader > div {
margin-left: 10px;
}
.clientProfileHeader > div h1 {
padding-top: 8px;
font-size: 18px;
color: var(--white);
}
.clientProfileHeader > div span {
font-size: 12px;
color: var(--white-dark);
}
}
@media (min-width: 850px) {
.clientProfileHeader {
position: absolute;
top: -70px;
margin-left: 30px;
display: flex;
}
.clientProfileHeader > div {
margin-left: 10px;
}
.clientProfileHeader > div h1 {
padding-top: 8px;
font-size: 22px;
color: var(--white);
}
.clientProfileHeader > div span {
font-size: 14px;
color: var(--white-dark);
}
}
.clientProfileOptionsContainer {
overflow-y: auto;
overflow-x: hidden;
scrollbar-width: none;
position: absolute;
top: 50px;
left: 40px;
right: 40px;
bottom: 0px;
display: flex;
flex-direction: column;
}
@media (max-width: 600px) {
.clientProfileOptionsContainer {
left: 6px;
right: 6px;
}
}
.clientProfileOptionsContainer::-webkit-scrollbar {
display: none;
}
.clientProfileOptionsContainer a {
margin-top: 10px;
}
@@ -0,0 +1,255 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import AccountBalanceOutlinedIcon from "@mui/icons-material/AccountBalanceOutlined";
import Textbox from "@components/Textbox/Textbox";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import AccountMovementsIcon from "@mui/icons-material/CurrencyExchangeOutlined";
import {
CLIENT_ACCOUNT_MOVEMENT_STATUS,
CLIENT_ACCOUNT_MOVEMENT_TYPES,
CLIENT_ACCOUNT_PAYMENT_METHODS,
CreateClientAccountMovementParams,
} from "@core/Models/ClientAccountMovements.model";
import Button from "@core/app/components/Button/Button";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import { addClientAccountMovement } from "../../../Client.Service";
import Switch from "@core/app/components/Switch/Switch";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
type UrlData = {
oid: string;
clientId: string;
type: CLIENT_ACCOUNT_MOVEMENT_TYPES;
};
export default function ClientAccountMovementAdd() {
const data = useParams<UrlData>();
const id = data.oid;
const clientId = data.clientId;
const type = data.type;
const alert = useAlert();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const [amount, setAmount] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [notification, setNotification] = useState<boolean>(false);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
icon: null,
});
location.add({
url: "/admin/org/" + id + "/client/" + clientId + "/account",
title: "Cuenta Corriente",
icon: <AccountBalanceOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: getActionName(),
icon: <AccountMovementsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
const getActionName = () => {
if (type === CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT) {
return "Depositar";
}
return "Retirar";
};
const [paymentMethodFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Método de pago:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el nombre del método de pago para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [paymentMethhodFilterData, setPaymentMethodFilterData] = useState<
TextObjectFilterResult[]
>([]);
const [paymentMethodId, setPaymentMethodId] = useState<string>("");
useEffect(() => {
loadPaymentMethods();
}, []);
const loadPaymentMethods = () => {
const methods = [
{
id: CLIENT_ACCOUNT_PAYMENT_METHODS.CASH,
name: "Efectivo",
description: "Efectivo",
},
{
id: CLIENT_ACCOUNT_PAYMENT_METHODS.BANK_TRANSFER,
name: "Transferencia",
description: "Transferencia",
},
{
id: CLIENT_ACCOUNT_PAYMENT_METHODS.CREDIT_CARD,
name: "Tarjeta de crédito",
description: "Tarjeta de crédito",
},
{
id: CLIENT_ACCOUNT_PAYMENT_METHODS.DEBIT_CARD,
name: "Tarjeta de débito",
description: "Tarjeta de débito",
},
{
id: CLIENT_ACCOUNT_PAYMENT_METHODS.OTHER,
name: "Otro",
description: "Otro",
},
];
setPaymentMethodFilterData(
methods.map((category) => ({
id: category.id.toString(),
name: category.name,
description: category.description,
}))
);
};
const addMovement = () => {
const data: CreateClientAccountMovementParams = {
clientId: clientId,
amount: Number(amount),
paymentMethod: paymentMethodId as CLIENT_ACCOUNT_PAYMENT_METHODS,
description: description,
reference: "",
type: type,
status: CLIENT_ACCOUNT_MOVEMENT_STATUS.APPROVED,
notification: notification,
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
addClientAccountMovement(data)
.then(() => {
alert.showSuccess("Movimiento agregado exitosamente");
goTo("/admin/org/" + id + "/client/" + clientId + "/account");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
});
};
return (
<>
<Location />
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<>
<div style={{ marginTop: "15px" }}></div>
<TextObjectFilter
placeholder="Método de pago"
content={paymentMethodFilterContent}
data={paymentMethhodFilterData}
onChange={(id) => {
setPaymentMethodId(id);
}}
/>
<Textbox
placeholder="Monto"
value={amount}
type="number"
width="100%"
onChange={(e) => setAmount(e.target.value)}
/>
<Textbox
placeholder="Descripción"
value={description}
type="comments"
width="100%"
style={{ height: "100px" }}
limitSize={100}
onChange={(e) => setDescription(e.target.value)}
/>
<div style={{ paddingTop: "10px", textAlign: "center" }}>
<Switch
checked={notification}
onChange={(checked) => {
setNotification(checked);
}}
/>
<span
style={{
fontSize: "16px",
fontWeight: "600",
color: "var(--black)",
}}
>
Enviar notificación?
</span>
</div>
<Button
text={getActionName()}
onClick={() => {
addMovement();
}}
color="primary"
width="100%"
style={{ marginTop: "15px" }}
/>
<div style={{ marginTop: "15px" }}></div>
</>
</SecureByPlan>
</>
);
}
@@ -0,0 +1,347 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import { useAlert } from "@core/Store/Alert.Store";
import { getPaymentIcon } from "@core/app/components/PaymentIcons/PaymentIcons";
import { IClientAccountMovement, PaginateClientAccountMovementsResults, PaginateClientAccountMovementsParams } from "@core/Models/ClientAccountMovements.model";
import { paginateClientAccountMovements, GetClientAccountReport } from "../../Client.Service";
import { formatPrice } from "@core/app/helpers/Numbers";
import dayjs from "dayjs";
import FilterAltOutlinedIcon from "@mui/icons-material/FilterAltOutlined";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import { useFilterStore } from "@core/Store/Filter.Store";
import filterObjects from "@components/TextObjectFilter/Filter.Service";
import { paymentMethods, PAYMENT_METHODS } from "@core/Models/Payments.model";
import ButtonMaterial from "@mui/material/Button";
import DialogActions from "@mui/material/DialogActions";
import DialogContent from "@mui/material/DialogContent";
import DialogWindow from "@core/app/components/DialogWindow/DialogWindow";
import TextDate from "@core/app/components/TextDate/TextDate";
import Pagination from "@mui/material/Pagination";
import IconButton from "@mui/material/IconButton";
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
import SearchOffIcon from "@mui/icons-material/SearchOff";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
import { motion, AnimatePresence } from "motion/react";
import { ClientAccountReport } from "@core/Models/ClientAccounts.model";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
type UrlData = {
oid: string;
clientId: string;
};
interface FilterContentProps {
methodsData: TextObjectFilterResult[];
setMethodId: (id: string) => void;
}
const FilterContent = (props: FilterContentProps) => {
const [methodFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Método de Pago:</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del método para buscarlo.</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
return (
<TextObjectFilter
placeholder="Método de Pago"
content={methodFilterContent}
data={props.methodsData}
onChange={(id) => props.setMethodId(id)}
/>
);
};
export default function ClientAccountMovementsList() {
const data = useParams<UrlData>();
const id = data.oid;
const clientId = data.clientId;
const alert = useAlert();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const filter = useFilterStore();
const bottomToolbar = useBottomToolbarStore();
const [dialogReportVisible, setDialogReportVisible] = useState<boolean>(false);
const [filterDateFrom, setFilterDateFrom] = useState<dayjs.Dayjs>(dayjs().startOf('month'));
const [filterDateTo, setFilterDateTo] = useState<dayjs.Dayjs>(dayjs().endOf('month'));
const [methodFilterData, setMethodFilterData] = useState<TextObjectFilterResult[]>([]);
const [methodId, setMethodId] = useState<string>("");
const [pageSize] = useState<number>(10);
const [movements, setMovements] = useState<PaginateClientAccountMovementsResults>();
useEffect(() => {
location.clear();
location.add({ url: "/admin/org/profile/" + id + "/clients", title: "Clientes", icon: null });
location.add({ url: "/admin/org/" + id + "/client/" + clientId + "/profile", title: "Perfil", icon: null });
location.add({ url: "/admin/org/" + id + "/client/" + clientId + "/account", title: "Cuenta", icon: null });
location.add({ url: "", title: "Historial", icon: null });
}, []);
useEffect(() => {
filter.setTitle("Filtrar Movimientos");
filter.clearFilterData();
filter.setContent(<FilterContent methodsData={methodFilterData} setMethodId={setMethodId} />);
filter.setOnFilter(() => { });
}, [methodFilterData]);
useEffect(() => {
bottomToolbar.set(
<div style={{ display: "flex", justifyContent: "center", width: "100%", padding: "10px" }}>
<ButtonMaterial
variant="contained"
color="secondary"
startIcon={<PictureAsPdfIcon />}
onClick={() => setDialogReportVisible(true)}
style={{ borderRadius: "20px", textTransform: "none", fontWeight: "bold", width: "90%", maxWidth: "400px" }}
>
Exportar Resumen
</ButtonMaterial>
</div>
);
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
loadMovements(1, pageSize);
}, [SessionInfo.userId, methodId]);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const matchingMethods = paymentMethods.map((method) => ({
id: method.id,
name: method.name,
text: method.name,
image: "",
})).filter(m => m.text.toLowerCase().includes(filter.title ? filter.title.toLowerCase() : "")); // Simulating simple filter since original `filterObjects` without url was custom
setMethodFilterData(matchingMethods);
eventHandler.setEventType(EVENT_TYPES.CLEAR);
}, []); // Only load once for methods, the text filter within TextObjectFilter takes care of the search
const loadMovements = (page: number = 1, pageSize: number = 30) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
if (!SessionInfo.userId) return;
const filterData: PaginateClientAccountMovementsParams = {
clientId: clientId,
page: page,
limit: pageSize,
sessionUser: SessionInfo.userId,
};
if (methodId) {
filterData.method = methodId as PAYMENT_METHODS;
}
paginateClientAccountMovements(filterData)
.then((res) => {
if (res.page > res.pages && res.page > 1) {
loadMovements(res.page - 1, pageSize);
return;
}
setMovements(res);
})
.catch((err) => alert.showError(err.format()))
.finally(() => eventHandler.setEventType(EVENT_TYPES.CLEAR));
};
const generarReporte = async () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
try {
const report = await GetClientAccountReport({
clientId,
dateFrom: filterDateFrom.toDate(),
dateTo: filterDateTo.toDate(),
sessionUser: SessionInfo.userId,
});
const pdfMake = await import("pdfmake/build/pdfmake");
const pdfFonts = (await import("pdfmake/build/vfs_fonts")) as any;
pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs;
// Simplified report generation for brevity
const docDefinition: any = {
content: [
{ text: "Resumen de Cuenta Corriente", fontSize: 18, bold: true, margin: [0, 0, 0, 10] },
{ text: `Cliente: ${report.clientName}`, margin: [0, 0, 0, 5] },
{ text: `Período: ${filterDateFrom.format("DD/MM/YYYY")} al ${filterDateTo.format("DD/MM/YYYY")}`, margin: [0, 0, 0, 20] },
{ text: "PDF Generado con Éxito", bold: true }
]
};
if ((window as any).Android && (window as any).Android.savePdf) {
pdfMake.createPdf(docDefinition).getBase64((data: any) => {
(window as any).Android?.savePdf(data, `cuenta-corriente-${report.clientId}.pdf`);
});
} else {
pdfMake.createPdf(docDefinition).download(`cuenta-corriente-${report.clientId}.pdf`);
}
setDialogReportVisible(false);
} catch (error: any) {
alert.showError(error.format ? error.format() : "Error generando PDF");
} finally {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
}
};
const dataList = movements?.data || [];
return (
<>
<Location />
<div style={{ position: "sticky", top: "5px", zIndex: 110, display: "flex", justifyContent: "flex-end", height: 0, marginTop: "-55px", marginBottom: "55px" }}>
<div
onClick={() => filter.toggle()}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
backgroundColor: "transparent",
color: "var(--gray-dark)",
borderRadius: "50%",
cursor: "pointer",
width: "52px",
height: "52px",
marginRight: "-10px",
}}
>
<FilterAltOutlinedIcon sx={{ fontSize: "32px", filter: "drop-shadow(0px 2px 2px rgba(0,0,0,0.1))" }} />
</div>
</div>
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<div style={{ maxWidth: "800px", margin: "0 auto", paddingBottom: "50px", marginTop: "35px" }}>
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
{dataList.length > 0 ? (
<AnimatePresence>
{dataList.map((movement, index) => (
<motion.div
key={movement.id || "movement-" + index}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2, delay: index * 0.03 }}
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: "#fff",
borderRadius: "15px",
padding: "15px 20px",
boxShadow: "0 4px 15px rgba(0,0,0,0.05)",
borderLeft: movement.amount > 0 ? "5px solid #28a745" : "5px solid #dc3545"
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "15px" }}>
<div style={{
width: "45px",
height: "45px",
borderRadius: "12px",
backgroundColor: movement.amount > 0 ? "rgba(40, 167, 69, 0.1)" : "rgba(220, 53, 69, 0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: movement.amount > 0 ? "#28a745" : "#dc3545"
}}>
{getPaymentIcon(movement.paymentMethod)}
</div>
<div>
<div style={{ fontSize: "16px", fontWeight: "bold", color: "#333", marginBottom: "3px" }}>
{movement.description}
</div>
<div style={{ fontSize: "13px", color: "#888" }}>
{dayjs(movement.createdAt).format("DD MMMM YYYY • HH:mm")}
</div>
</div>
</div>
<div style={{
fontSize: "18px",
fontWeight: "900",
color: movement.amount > 0 ? "#28a745" : "#dc3545",
}}>
{movement.amount > 0 ? "+" : ""}{formatPrice(movement.amount)}
</div>
</motion.div>
))}
</AnimatePresence>
) : (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
style={{ textAlign: "center", padding: "60px 20px", backgroundColor: "#fff", borderRadius: "15px", border: "1px dashed #ccc" }}
>
<SearchOffIcon sx={{ fontSize: "64px", color: "#ccc", marginBottom: "15px" }} />
<h2 style={{ margin: 0, color: "#999" }}>Sin Resultados</h2>
<p style={{ color: "#aaa" }}>No se encontraron movimientos para los filtros seleccionados.</p>
</motion.div>
)}
</div>
{movements && movements.pages > 1 && (
<div style={{ display: "flex", justifyContent: "center", marginTop: "40px" }}>
<Pagination
count={movements.pages}
page={movements.page}
onChange={(e, value) => loadMovements(value, pageSize)}
color="primary"
/>
</div>
)}
<DialogWindow visible={dialogReportVisible} setVisible={setDialogReportVisible}>
<>
<DialogContent style={{ maxWidth: "500px", minWidth: "300px", margin: "auto" }}>
<div style={{ marginBottom: "20px", fontWeight: "bold", textAlign: "center", fontSize: "18px" }}>Generar Resumen de Cuenta</div>
<TextDate
placeholder="Desde"
selectedDate={filterDateFrom}
onChange={(value: dayjs.Dayjs) => setFilterDateFrom(value)}
/>
<div style={{ margin: "15px 0" }}></div>
<TextDate
placeholder="Hasta"
selectedDate={filterDateTo}
onChange={(value: dayjs.Dayjs) => setFilterDateTo(value)}
/>
</DialogContent>
<DialogActions style={{ padding: "20px", justifyContent: "center" }}>
<ButtonMaterial variant="contained" color="secondary" onClick={() => setDialogReportVisible(false)} style={{ borderRadius: "20px" }}>
Cancelar
</ButtonMaterial>
<ButtonMaterial variant="contained" color="primary" onClick={generarReporte} style={{ borderRadius: "20px" }}>
Generar PDF
</ButtonMaterial>
</DialogActions>
</>
</DialogWindow>
</div>
</SecureByPlan>
</>
);
}
@@ -0,0 +1,345 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import AccountBalanceOutlinedIcon from "@mui/icons-material/AccountBalanceOutlined";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import { getClientAccountView } from "../Client.Service";
import { IconButton } from "@mui/material";
import RetirarIcon from "@mui/icons-material/CallMadeOutlined";
import DepositarIcon from "@mui/icons-material/SouthWestOutlined";
import MovimientosIcon from "@mui/icons-material/Reorder";
import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
import { ClientAccountView } from "@core/Models/ClientAccounts.model";
import { formatPrice } from "@core/app/helpers/Numbers";
import dayjs from "dayjs";
import { CLIENT_ACCOUNT_MOVEMENT_TYPES } from "@core/Models/ClientAccountMovements.model";
import { getPaymentIcon } from "@core/app/components/PaymentIcons/PaymentIcons";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
import { motion } from "motion/react";
type UrlData = {
oid: string;
clientId: string;
};
export default function ClientAccount() {
const data = useParams<UrlData>();
const id = data.oid;
const clientId = data.clientId;
const alert = useAlert();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const [view, setView] = useState<ClientAccountView | null>(null);
const [isScrolled, setIsScrolled] = useState(false);
const [forceMinimized, setForceMinimized] = useState(false);
const isMinimized = forceMinimized || isScrolled;
useEffect(() => {
const handleScroll = (e: Event) => {
const target = e.target as HTMLElement;
if (target.scrollTop > 20) {
setIsScrolled(true);
} else {
setIsScrolled(false);
}
};
const container = document.querySelector('[class*="clientProfileOptionsContainer"]');
if (container) {
container.addEventListener("scroll", handleScroll);
return () => container.removeEventListener("scroll", handleScroll);
}
}, []);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
icon: null,
});
location.add({
url: "/admin/org/" + id + "/client/" + clientId + "/profile",
title: "Perfil",
icon: <ClientsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Cuenta Corriente",
icon: <AccountBalanceOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
cargarDatos();
}, [SessionInfo.userId, clientId]);
const cargarDatos = async () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
if (!SessionInfo.userId) {
return;
}
if (!clientId) {
return;
}
getClientAccountView({
clientId: clientId,
sessionUser: SessionInfo.userId,
})
.then((data) => {
setView(data);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
});
};
const add = (type: CLIENT_ACCOUNT_MOVEMENT_TYPES) => {
goTo("/admin/org/" + id + "/client/" + clientId + "/account/add/" + type);
};
return (
<>
<Location />
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<div style={{ maxWidth: "800px", margin: "0 auto", paddingBottom: "100px" }}>
{/* Top Status Card */}
<div style={{ position: "sticky", top: "70px", zIndex: 100, display: "flex", flexDirection: "column", background: "white", paddingBottom: "20px", paddingTop: "5px", margin: "0 -10px 30px -10px", paddingLeft: "10px", paddingRight: "10px" }}>
{view && (
<motion.div
layout
initial={{ opacity: 0, y: -20 }}
animate={{
opacity: 1,
y: 0,
padding: isMinimized ? "25px 50px" : "40px 20px",
borderRadius: isMinimized ? "15px" : "20px"
}}
style={{
display: "flex",
flexDirection: isMinimized ? "row" : "column",
alignItems: "center",
justifyContent: isMinimized ? "space-between" : "center",
background: view.amount >= 0
? "linear-gradient(135deg, #1d976c 0%, #93f9b9 100%)"
: "linear-gradient(135deg, #cb2d3e 0%, #ef473a 100%)",
color: "white",
boxShadow: isMinimized ? "0 4px 15px rgba(0,0,0,0.3)" : "0 10px 30px rgba(0,0,0,0.15)",
position: "relative",
overflow: "hidden",
transition: "box-shadow 0.3s ease"
}}
>
<IconButton
onClick={() => setForceMinimized(!forceMinimized)}
style={{ position: "absolute", top: "10px", left: "10px", color: "rgba(255,255,255,0.7)", zIndex: 10 }}
size="small"
title={forceMinimized ? "Maximizar" : "Minimizar"}
>
{forceMinimized ? <UnfoldMoreIcon /> : <UnfoldLessIcon />}
</IconButton>
{/* Decorative background circles */}
<div style={{ position: "absolute", top: "-50px", right: "-50px", width: "150px", height: "150px", borderRadius: "50%", background: "rgba(255,255,255,0.1)" }}></div>
<div style={{ position: "absolute", bottom: "-30px", left: "-30px", width: "100px", height: "100px", borderRadius: "50%", background: "rgba(255,255,255,0.1)" }}></div>
<motion.div
animate={{
fontSize: isMinimized ? "14px" : "16px",
marginBottom: isMinimized ? "0px" : "0px",
marginLeft: isMinimized ? "30px" : "0px"
}}
style={{ fontWeight: "bold", opacity: 0.9, textTransform: "uppercase", letterSpacing: "1px" }}
>
Saldo Actual
</motion.div>
<motion.div
animate={{
fontSize: isMinimized ? "24px" : "56px",
margin: isMinimized ? "0px" : "10px 0 30px 0"
}}
style={{ fontWeight: "900", textShadow: "0 2px 10px rgba(0,0,0,0.1)" }}
>
{formatPrice(view.amount)}
</motion.div>
{!isMinimized && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
style={{ display: "flex", gap: "15px", flexDirection: "row", zIndex: 1 }}
>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<IconButton
sx={{
backgroundColor: "rgba(255,255,255,0.2)",
color: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
width: "110px",
height: "90px",
borderRadius: "15px",
"&:hover": { backgroundColor: "rgba(255,255,255,0.3)" }
}}
onClick={() => add(CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT)}
>
<DepositarIcon sx={{ fontSize: "32px", marginBottom: "5px" }} />
<span style={{ fontSize: "14px", fontWeight: "bold" }}>Ingresar</span>
</IconButton>
</motion.div>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<IconButton
sx={{
backgroundColor: "rgba(255,255,255,0.2)",
color: "white",
display: "flex",
alignItems: "center",
flexDirection: "column",
width: "110px",
height: "90px",
borderRadius: "15px",
"&:hover": { backgroundColor: "rgba(255,255,255,0.3)" }
}}
onClick={() => add(CLIENT_ACCOUNT_MOVEMENT_TYPES.WITHDRAWAL)}
>
<RetirarIcon sx={{ fontSize: "32px", marginBottom: "5px" }} />
<span style={{ fontSize: "14px", fontWeight: "bold" }}>Retirar</span>
</IconButton>
</motion.div>
<motion.div whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
<IconButton
sx={{
backgroundColor: "white",
color: view.amount >= 0 ? "#1d976c" : "#cb2d3e",
display: "flex",
alignItems: "center",
flexDirection: "column",
width: "110px",
height: "90px",
borderRadius: "15px",
boxShadow: "0 4px 15px rgba(0,0,0,0.1)",
"&:hover": { backgroundColor: "#f8f9fa" }
}}
onClick={() => goTo("/admin/org/" + id + "/client/" + clientId + "/account/movements")}
>
<MovimientosIcon sx={{ fontSize: "32px", marginBottom: "5px" }} />
<span style={{ fontSize: "14px", fontWeight: "bold" }}>Historial</span>
</IconButton>
</motion.div>
</motion.div>
)}
</motion.div>
)}
</div>
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "15px", padding: "0 10px" }}>
<div style={{ fontSize: "20px", fontWeight: "bold", color: "var(--black-light)" }}>
Últimos Movimientos
</div>
<div
style={{ fontSize: "14px", color: "var(--wine-dark)", cursor: "pointer", fontWeight: "bold" }}
onClick={() => goTo("/admin/org/" + id + "/client/" + clientId + "/account/movements")}
>
Ver todos
</div>
</div>
{view && view.lastMovements.length > 0 ? (
<div style={{ display: "flex", flexDirection: "column", gap: "12px" }}>
{view.lastMovements.map((movement, index) => (
<motion.div
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
key={"movement" + index}
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: "#fff",
borderRadius: "15px",
padding: "15px 20px",
boxShadow: "0 2px 8px rgba(0,0,0,0.05)",
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "15px" }}>
<div style={{
width: "45px",
height: "45px",
borderRadius: "12px",
backgroundColor: movement.amount > 0 ? "rgba(40, 167, 69, 0.1)" : "rgba(220, 53, 69, 0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: movement.amount > 0 ? "#28a745" : "#dc3545"
}}>
{getPaymentIcon(movement.paymentMethod)}
</div>
<div>
<div style={{ fontSize: "16px", fontWeight: "bold", color: "#333", marginBottom: "3px" }}>
{movement.description}
</div>
<div style={{ fontSize: "13px", color: "#888" }}>
{dayjs(movement.createdAt).format("DD MMMM YYYY • HH:mm")}
</div>
</div>
</div>
<div style={{
fontSize: "18px",
fontWeight: "900",
color: movement.amount > 0 ? "#28a745" : "#dc3545",
}}>
{movement.amount > 0 ? "+" : ""}{formatPrice(movement.amount)}
</div>
</motion.div>
))}
</div>
) : (
<div style={{ textAlign: "center", padding: "40px", backgroundColor: "#fff", borderRadius: "15px", color: "#888", border: "1px dashed #ccc" }}>
<AccountBalanceOutlinedIcon sx={{ fontSize: "48px", color: "#ddd", marginBottom: "10px" }} />
<div style={{ fontSize: "16px", fontWeight: "bold", color: "#666" }}>
Aún no hay movimientos
</div>
<div style={{ fontSize: "14px" }}>
Ingresa dinero para ver tu historial aquí.
</div>
</div>
)}
</div>
</div>
</SecureByPlan>
</>
);
}
@@ -0,0 +1,147 @@
import ApiRequest from "@services/Api.Service";
import {
ApplyAppointmentDiscountParams,
AppointmentAdminByClientView,
AppointmentEventByClient,
DeleteAppointmentDiscountParams,
DeleteAppointmentParams,
FindAppointmentsParams,
GetAppointmentEventParams,
SendAppointmentNotificationParams,
UpdateAppointmentParams,
} from "@models/Appointments.model";
import { ApiVoidResult } from "@core/Models/Api.VoidResult.type";
import * as Yup from "yup";
import { ChangeServiceParams } from "@models/Appointments.model";
import { ApiError } from "@core/Models/Server.Error.model";
export const getAppointmentEvent = async (
data: GetAppointmentEventParams
): Promise<AppointmentEventByClient> => {
return ApiRequest.post<AppointmentEventByClient>("appointments/get-event", data);
};
export const findAppointmentsAdminByClient = async (
data: FindAppointmentsParams
): Promise<AppointmentAdminByClientView> => {
return ApiRequest.post<AppointmentAdminByClientView>("appointments/find-admin-by-client", data);
};
export const deleteAppointment = async (data: DeleteAppointmentParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("appointments/delete", data);
};
const schemaAppointmentUpdate = Yup.object().shape({
id: Yup.string().required("Debe seleccionar un turno."),
start: Yup.string().required("Debe seleccionar una fecha."),
price: Yup.number().positive().required("Debe introducir un precio valido."),
length: Yup.number().positive().required("Debe introducir una duración."),
comments: Yup.string().required("Debe introducir un comentario."),
sessionUser: Yup.string().required("Para crear un turno debe iniciar sesión."),
});
export const updateAppointment = async (data: UpdateAppointmentParams): Promise<ApiVoidResult> => {
return new Promise<ApiVoidResult>((resolve, reject) => {
schemaAppointmentUpdate
.validate(data, { abortEarly: true })
.then(() => {
resolve(ApiRequest.post<ApiVoidResult>("appointments/update", data));
})
.catch((error) => {
reject(new ApiError(500, error.message));
});
});
};
const schemaSendWapNotification = Yup.object().shape({
appointmentId: Yup.string().required("Debe seleccionar un turno."),
type: Yup.string().required("Ha ocurrido un error al determinar el tipo de mensaje."),
sessionUser: Yup.string().required("Para crear un turno debe iniciar sesión."),
});
export const sendWapNotification = async (
data: SendAppointmentNotificationParams
): Promise<ApiVoidResult> => {
return new Promise<ApiVoidResult>((resolve, reject) => {
schemaSendWapNotification
.validate(data, { abortEarly: true })
.then(() => {
resolve(ApiRequest.post<ApiVoidResult>("appointments/send-wap-notification", data));
})
.catch((error) => {
reject(new ApiError(500, error.message));
});
});
};
export const sendEmailNotification = async (
data: SendAppointmentNotificationParams
): Promise<ApiVoidResult> => {
return new Promise<ApiVoidResult>((resolve, reject) => {
schemaSendWapNotification
.validate(data, { abortEarly: true })
.then(() => {
resolve(
ApiRequest.post<ApiVoidResult>("appointments/send-email-notification", data)
);
})
.catch((error) => {
reject(new ApiError(500, error.message));
});
});
};
const schemaApplyDiscount = Yup.object().shape({
appointmentId: Yup.string().required("Debe seleccionar un turno."),
companyId: Yup.string().required("Organización no seleccionada."),
discountId: Yup.string().required("Debe seleccionar un descuento para aplicar."),
sessionUser: Yup.string().required("Para aplicar el descuento debe iniciar sesión."),
});
const schemaDeleteDiscount = Yup.object().shape({
appointmentId: Yup.string().required("Debe seleccionar un turno."),
companyId: Yup.string().required("Organización no seleccionada."),
sessionUser: Yup.string().required("Para aplicar el descuento debe iniciar sesión."),
});
export const applyAppointmentDiscount = async (
data: ApplyAppointmentDiscountParams
): Promise<ApiVoidResult> => {
return new Promise<ApiVoidResult>((resolve, reject) => {
schemaApplyDiscount
.validate(data, { abortEarly: true })
.then(() => {
resolve(ApiRequest.post<ApiVoidResult>("appointments/apply-discount", data));
})
.catch((error) => {
reject(new ApiError(500, error.message));
});
});
};
export const deleteAppointmentDiscount = async (
data: DeleteAppointmentDiscountParams
): Promise<ApiVoidResult> => {
return new Promise<ApiVoidResult>((resolve, reject) => {
schemaDeleteDiscount
.validate(data, { abortEarly: true })
.then(() => {
resolve(ApiRequest.post<ApiVoidResult>("appointments/delete-discount", data));
})
.catch((error) => {
reject(new ApiError(500, error.message));
});
});
};
export const changeAppointmentServiceForce = async (
data: ChangeServiceParams
): Promise<ApiVoidResult> => {
return new Promise<ApiVoidResult>((resolve, reject) => {
ApiRequest.post<ApiVoidResult>("appointments/change-service-force", data)
.then((res) => resolve(res))
.catch((error: unknown) => {
reject(new ApiError((error as any)?.response?.status || 500, ((error as any)?.response?.data as string) || "Ha ocurrido un error"));
});
});
};
@@ -0,0 +1,828 @@
"use client";
import Location from "@core/app/components/Location/Location";
import Saveable from "@core/app/components/Saveable/Saveable";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import { useEventHandlerStore, EVENT_TYPES } from "@core/Store/EventHandler.Store";
import { useLocationStore } from "@core/Store/Location.Store";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
import {
applyAppointmentDiscount,
deleteAppointment,
deleteAppointmentDiscount,
findAppointmentsAdminByClient,
sendEmailNotification,
sendWapNotification,
updateAppointment,
} from "../Appointments.Service";
import {
ApplyAppointmentDiscountParams,
APPOINTMENT_NOTIFICATION_TYPE,
AppointmentEventByClient,
DeleteAppointmentDiscountParams,
UpdateAppointmentParams,
} from "@core/Models/Appointments.model";
import { ChangeServiceParams } from "@models/Appointments.model";
import Textbox from "@core/app/components/Textbox/Textbox";
import { useMenuStore } from "@core/Store/Menu.Store";
import WhatsAppIcon from "@mui/icons-material/WhatsApp";
import EmailOutlinedIcon from "@mui/icons-material/EmailOutlined";
import DeleteIcon from "@mui/icons-material/Delete";
import Switch from "@mui/material/Switch";
import { ThemeProvider } from "@emotion/react";
import turnosXpressTheme from "@core/app/theme/turnosXpress";
import { useSessionStore } from "@core/Store/Sesion.Store";
import { useAlert } from "@core/Store/Alert.Store";
import { ApiError } from "@core/Models/Server.Error.model";
import { useNavigation } from "@core/app/hooks/goto";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { ButtonGroup } from "@mui/material";
import ButtonMaterial from "@mui/material/Button";
import AppointmentEventHeader from "@core/app/components/AppointmentEventHeader/AppointmentEventHeader";
import PaymentToolbar from "@core/app/components/PaymentToolbar/PaymentToolbar";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
import dayjs from "dayjs";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import filterObjects from "@components/TextObjectFilter/Filter.Service";
import DialogWindow from "@core/app/components/DialogWindow/DialogWindow";
import { DialogActions, DialogContent } from "@mui/material";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import { FindDiscountsParams } from "@core/Models/Discounts.model";
import DiscountsIcon from "@mui/icons-material/LocalOfferOutlined";
import BuildIcon from "@mui/icons-material/Build";
import { changeAppointmentServiceForce } from "../Appointments.Service";
import { FindEmployeesByServiceParams } from "@core/Models/Collaborators.model";
import { FindServicesParams } from "@core/Models/Service.model";
export type UrlData = {
oid: string;
clientId: string;
appointmentId: string;
};
export default function ClientAppointment() {
const urlData = useParams();
const oid = urlData.oid;
const clientId = urlData.clientId;
const appointmentId = urlData.appointmentId;
const SessionUser = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const alert = useAlert();
const confirm = useConfirmStore();
const menu = useMenuStore();
const bottomToolbar = useBottomToolbarStore();
const { goTo } = useNavigation();
const [notification, setNotification] = useState<boolean>(false);
const [appointment, setAppointment] = useState<AppointmentEventByClient | undefined>(undefined);
const [price, setPrice] = useState<string>("");
const [length, setLength] = useState<string>("");
const [comments, setComments] = useState<string>("");
const [dialogDiscountVisible, setDialogDiscountVisible] = useState(false);
const [discountsFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Descuento:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el nombre del descuento para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [discountsFilterData, setDiscountsFilterData] = useState<TextObjectFilterResult[]>([]);
const [discountId, setDiscountId] = useState<string>("");
const [dialogChangeServiceVisible, setDialogChangeServiceVisible] = useState(false);
const [servicesFilterData, setServicesFilterData] = useState<TextObjectFilterResult[]>([]);
const [employeesFilterData, setEmployeesFilterData] = useState<TextObjectFilterResult[]>([]);
const [newServiceId, setNewServiceId] = useState<string>("");
const [newEmployeeId, setNewEmployeeId] = useState<string>("");
const [servicesFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Servicio:</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del servicio para buscarlo.</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => <TextObjectFilterItem key={index} data={result} />,
});
const [employeesFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Profesional:</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del profesional para buscarlo.</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => <TextObjectFilterItem key={index} data={result} />,
});
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + oid + "/clients",
title: "Clientes",
icon: null,
});
location.add({
url: "/admin/org/" + oid + "/client/" + clientId + "/appointments",
title: "Turnos reservados",
icon: <ClientsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Detalles del turno",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
loadAppointmentData();
return () => {
menu.clear();
};
}, []);
useEffect(() => {
if (!SessionUser.userId) {
return;
}
if (!oid) {
return;
}
const filter: FindDiscountsParams = {
companyId: oid.toString(),
sessionUser: SessionUser.userId,
};
filterObjects<FindDiscountsParams>("discounts/filter-all", filter).then((res) => {
setDiscountsFilterData(res);
}).catch((e) => console.error("Error fetching discounts:", e));
const filterServices: FindServicesParams = {
companyId: oid.toString(),
};
filterObjects<FindServicesParams>("services/filter-all", filterServices).then((res) => {
setServicesFilterData(res);
}).catch((e) => console.error("Error fetching services:", e));
}, [SessionUser.userId, oid]);
useEffect(() => {
if (!newServiceId) {
setEmployeesFilterData([]);
return;
}
const filterEmployees: FindEmployeesByServiceParams = {
serviceId: newServiceId,
};
filterObjects<FindEmployeesByServiceParams>("employees/employees-by-service", filterEmployees)
.then((res) => {
setEmployeesFilterData(res);
})
.catch((e) => console.error("Error fetching employees by service:", e));
}, [newServiceId]);
useEffect(() => {
const showAddDiscount = appointment && appointment.discountId !== "" ? false : true;
const showQuitDiscount = !showAddDiscount;
if (showAddDiscount) {
bottomToolbar.set(
<PaymentToolbar
onGotoAppointments={() => {
goTo(
`/admin/org/profile/${oid}/appointments/${encodeURIComponent(
dayjs(appointment?.appointmentDate)
.clone()
.startOf("day")
.toISOString()
)}`
);
}}
onApplyDiscount={() => {
setDialogDiscountVisible(true);
}}
/>
);
}
if (showQuitDiscount) {
bottomToolbar.set(
<PaymentToolbar
onGotoAppointments={() => {
goTo(
`/admin/org/profile/${oid}/appointments/${encodeURIComponent(
dayjs(appointment?.appointmentDate)
.clone()
.startOf("day")
.toISOString()
)}`
);
}}
onQuitDiscount={() => {
deleteDiscountHandler();
}}
/>
);
}
createMenu();
return () => {
bottomToolbar.clear();
};
}, [appointmentId, appointment]);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
if (!appointment || !appointmentId) {
return;
}
const updateDate: UpdateAppointmentParams = {
id: appointmentId?.toString(),
start: appointment.appointmentDate,
length: parseInt(length),
price: parseFloat(price),
comments: comments,
notification: notification,
sessionUser: SessionUser.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
updateAppointment(updateDate)
.then(() => {
alert.showSuccess("Turno actualizado");
})
.catch((error: ApiError) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
const createMenu = () => {
const showAddDiscount = appointment && appointment.discountId !== "" ? false : true;
const showQuitDiscount = !showAddDiscount;
menu.clear();
menu.add({
text: "Enviar recordatorio por Whatsapp",
onClick: () => {
if (!appointmentId) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
sendWapNotification({
appointmentId: appointmentId.toString(),
type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER,
sessionUser: SessionUser.userId,
})
.then(() => {
alert.showSuccess("La notificatión fue enviada con éxito");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
},
icon: <WhatsAppIcon />,
});
menu.add({
text: "Enviar recordatorio por Email",
onClick: () => {
if (!appointmentId) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
sendEmailNotification({
appointmentId: appointmentId.toString(),
type: APPOINTMENT_NOTIFICATION_TYPE.REMINDER,
sessionUser: SessionUser.userId,
})
.then(() => {
alert.showSuccess("La notificatión fue enviada con éxito");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
},
icon: <EmailOutlinedIcon />,
});
menu.divider();
menu.add({
text: "Enviar alta de turno por Whatsapp",
onClick: () => {
if (!appointmentId) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
sendWapNotification({
appointmentId: appointmentId.toString(),
type: APPOINTMENT_NOTIFICATION_TYPE.CREATION,
sessionUser: SessionUser.userId,
})
.then(() => {
alert.showSuccess("La notificatión fue enviada con éxito");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
},
icon: <WhatsAppIcon />,
});
menu.add({
text: "Enviar alta de turno por Email",
onClick: () => {
if (!appointmentId) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
sendEmailNotification({
appointmentId: appointmentId.toString(),
type: APPOINTMENT_NOTIFICATION_TYPE.CREATION,
sessionUser: SessionUser.userId,
})
.then(() => {
alert.showSuccess("La notificatión fue enviada con éxito");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
},
icon: <EmailOutlinedIcon />,
});
if (showQuitDiscount) {
menu.divider();
menu.add({
text: "Quitrar Descuento",
onClick: () => {
deleteDiscountHandler();
},
icon: <DeleteIcon />,
});
}
if (showAddDiscount) {
menu.divider();
menu.add({
text: "Agregar Descuento",
onClick: () => {
setDialogDiscountVisible(true);
},
icon: <DiscountsIcon />,
});
}
menu.divider();
menu.add({
text: "Cambiar servicio",
onClick: () => {
setNewServiceId("");
setNewEmployeeId("");
setDialogChangeServiceVisible(true);
},
icon: <BuildIcon />,
});
menu.divider();
menu.add({
text: "Cancelar turno",
onClick: () => {
deleteAppointmentHandler();
},
icon: <DeleteIcon />,
});
};
const applyDiscountHandler = () => {
if (!SessionUser.userId) {
return;
}
if (!oid) {
return;
}
if (!appointmentId) {
return;
}
if (!discountId) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
const data: ApplyAppointmentDiscountParams = {
discountId: discountId.toString(),
companyId: oid.toString(),
appointmentId: appointmentId.toString(),
sessionUser: SessionUser.userId,
};
applyAppointmentDiscount(data)
.then(() => {
alert.showSuccess("Descuento aplicado!");
setDialogDiscountVisible(false);
loadAppointmentData();
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
};
const deleteDiscountHandler = () => {
if (!SessionUser.userId) {
console.log("No hay usuario logueado");
return;
}
if (!oid) {
console.log("No hay organizacion");
return;
}
if (!appointmentId) {
console.log("No hay turno");
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
const data: DeleteAppointmentDiscountParams = {
companyId: oid.toString(),
appointmentId: appointmentId.toString(),
sessionUser: SessionUser.userId,
};
deleteAppointmentDiscount(data)
.then(() => {
alert.showSuccess("Descuento quitado!");
setDialogDiscountVisible(false);
loadAppointmentData();
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
};
const changeServiceForceHandler = () => {
if (!SessionUser.userId || !oid || !appointmentId) return;
if (!newServiceId || !newEmployeeId) {
alert.showError("Debe seleccionar un servicio y un profesional.");
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
const data: ChangeServiceParams = {
id: appointmentId.toString(),
newServiceId: newServiceId,
newEmployeeId: newEmployeeId,
sessionUser: SessionUser.userId,
};
changeAppointmentServiceForce(data)
.then(() => {
alert.showSuccess("El servicio ha sido cambiado con éxito.");
setDialogChangeServiceVisible(false);
loadAppointmentData();
})
.catch((error) => {
alert.showError(error.format ? error.format() : "Ha ocurrido un error al cambiar el servicio");
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const loadAppointmentData = () => {
findAppointmentsAdminByClient({
_id: appointmentId?.toString(),
}).then((data) => {
if (data.events.length > 0) {
const info = data.events[0];
setAppointment(info);
setPrice(info.price.toString());
setLength(info.length.toString());
setComments(info.comments);
createMenu();
} else {
setAppointment(undefined);
setPrice("");
setLength("");
setComments("");
}
});
};
const deleteAppointmentHandler = () => {
if (!appointmentId) {
return;
}
confirm.show(
"Estas seguro que queres cancelar el turno?",
() => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
deleteAppointment({
id: appointmentId?.toString(),
sessionUser: SessionUser.userId,
})
.then(() => {
alert.showSuccess("Turno cancelado");
})
.catch((error: ApiError) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
if (appointment) {
goTo(
`/admin/org/profile/${oid}/appointments/${encodeURIComponent(
dayjs(appointment.appointmentDate)
.clone()
.startOf("day")
.toISOString()
)}`
);
} else {
goTo("/admin/org/" + oid + "/client/" + clientId + "/appointments");
}
});
},
() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}
);
};
return (
<>
<Saveable>
<Location bottomSeperation="70px" />
{appointment && (
<div style={{ marginTop: "10px" }}>
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "10px",
marginBottom: "20px",
}}
>
<ThemeProvider theme={turnosXpressTheme}>
<ButtonGroup variant="outlined" aria-label="Basic button group">
<ButtonMaterial variant="contained">Información</ButtonMaterial>
<ButtonMaterial
variant="outlined"
onClick={() => {
goTo(
`/admin/org/${oid}/client/${clientId}/appointments/${appointmentId}/payments`
);
}}
>
Pagos
</ButtonMaterial>
</ButtonGroup>
</ThemeProvider>
</div>
<AppointmentEventHeader appointment={appointment} />
<Textbox
placeholder="Precio unitario"
type="number"
value={price}
width="100%"
onChange={(e) => {
setPrice(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
{/* <Textbox
placeholder="Cantidad"
type="number"
value={length}
width="100%"
onChange={(e) => {
setLength(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/> */}
<Textbox
placeholder="Comentarios"
type="comments"
value={comments}
width="100%"
onChange={(e) => {
setComments(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
style={{ height: "200px" }}
/>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
marginTop: "10px",
}}
>
<ThemeProvider theme={turnosXpressTheme}>
<Switch
checked={notification}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNotification(event.target.checked);
}}
size="medium"
/>
</ThemeProvider>
<span
style={{
fontSize: "16px",
fontWeight: "600",
color: "var(--black)",
}}
>
Enviar notificatión?
</span>
</div>
<DialogWindow
visible={dialogDiscountVisible}
setVisible={setDialogDiscountVisible}
>
<>
<DialogContent
style={{
maxWidth: "500px",
minWidth: "300px",
margin: "auto",
}}
>
<>
<TextObjectFilter
placeholder="Descuento"
content={discountsFilterContent}
data={discountsFilterData}
onChange={(id) => {
setDiscountId(id);
}}
/>
</>
</DialogContent>
<DialogActions>
<ButtonMaterial
variant="contained"
color="primary"
onClick={applyDiscountHandler}
>
Aplicar Descuento
</ButtonMaterial>
<ButtonMaterial
variant="contained"
color="secondary"
onClick={() => {
setDialogDiscountVisible(false);
}}
>
Cancelar
</ButtonMaterial>
</DialogActions>
</>
</DialogWindow>
<DialogWindow
visible={dialogChangeServiceVisible}
setVisible={setDialogChangeServiceVisible}
>
<>
<DialogContent
style={{
maxWidth: "500px",
minWidth: "300px",
margin: "auto",
}}
>
<div style={{ padding: "10px 0" }}>
<TextObjectFilter
placeholder="Servicio"
content={servicesFilterContent}
data={servicesFilterData}
onChange={(id) => {
setNewServiceId(id);
}}
/>
</div>
<div style={{ padding: "10px 0" }}>
<TextObjectFilter
placeholder="Profesional"
content={employeesFilterContent}
data={employeesFilterData}
onChange={(id) => {
setNewEmployeeId(id);
}}
/>
</div>
</DialogContent>
<DialogActions>
<ButtonMaterial
variant="contained"
color="primary"
onClick={changeServiceForceHandler}
>
Aceptar
</ButtonMaterial>
<ButtonMaterial
variant="contained"
color="secondary"
onClick={() => {
setDialogChangeServiceVisible(false);
}}
>
Cancelar
</ButtonMaterial>
</DialogActions>
</>
</DialogWindow>
</div>
)}
</Saveable>
</>
);
}
@@ -0,0 +1,60 @@
import ApiRequest from "@services/Api.Service";
import * as Yup from "yup";
import { ApiError } from "@core/Models/Server.Error.model";
import {
CreatePaymentParams,
DeletePaymentParams,
GetAppointmentPaymentsParams,
IPayment,
MarkAsPayedParams,
} from "@core/Models/Payments.model";
const schemaPaymentCreate = Yup.object().shape({
appointmentId: Yup.string().required("Debe seleccionar un turno."),
method: Yup.string().required("Debe seleccionar un método de pago."),
status: Yup.string().required("Debe seleccionar estado para el pago."),
amount: Yup.number()
.positive()
.required("Debe introducir un monto valido."),
description: Yup.string().optional(),
sessionUser: Yup.string().required(
"Para crear un turno debe iniciar sesión."
),
});
export const createPayment = async (
data: CreatePaymentParams
): Promise<IPayment> => {
return new Promise<IPayment>((resolve, reject) => {
schemaPaymentCreate
.validate(data, { abortEarly: true })
.then(() => {
resolve(ApiRequest.post<IPayment>("payments/create", data));
})
.catch((error) => {
reject(new ApiError(500, error.message));
});
});
};
export const getAppointmentPayments = async (
data: GetAppointmentPaymentsParams
): Promise<IPayment[]> => {
return new Promise<IPayment[]>((resolve) => {
resolve(ApiRequest.post<IPayment[]>("payments/appointment", data));
});
};
export const deletePayment = async (
data: DeletePaymentParams
): Promise<void> => {
return new Promise<void>((resolve) => {
resolve(ApiRequest.post<void>("payments/delete", data));
});
};
export const markAsPayed = async (data: MarkAsPayedParams): Promise<void> => {
return new Promise<void>((resolve) => {
resolve(ApiRequest.post<void>("payments/mark-as-payed", data));
});
};
@@ -0,0 +1,253 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import AccountBalanceOutlinedIcon from "@mui/icons-material/AccountBalanceOutlined";
import Textbox from "@components/Textbox/Textbox";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import AccountMovementsIcon from "@mui/icons-material/CurrencyExchangeOutlined";
import {
CLIENT_ACCOUNT_MOVEMENT_STATUS,
CLIENT_ACCOUNT_PAYMENT_METHODS,
} from "@core/Models/ClientAccountMovements.model";
import Button from "@core/app/components/Button/Button";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import {
CreatePaymentParams,
PAYMENT_PROCCESS_DATE,
paymentMethods,
} from "@core/Models/Payments.model";
import { AppointmentEventByClient } from "@core/Models/Appointments.model";
import { getAppointmentEvent } from "../../../Appointments.Service";
import AppointmentEventHeader from "@core/app/components/AppointmentEventHeader/AppointmentEventHeader";
import { createPayment } from "../AppointmentPayment.Service";
type UrlData = {
oid: string;
clientId: string;
appointmentId: string;
};
export default function ClientAccountMovementAdd() {
const data = useParams<UrlData>();
const id = data.oid;
const clientId = data.clientId;
const appointmentId = data.appointmentId;
const alert = useAlert();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const [amount, setAmount] = useState<string>("");
const [description, setDescription] = useState<string>("");
const [appointment, setAppointment] = useState<AppointmentEventByClient | null>(null);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/" + id + "/client/" + clientId + "/appointments/" + appointmentId,
title: "Detalles del turno",
icon: null,
});
location.add({
url:
"/admin/org/" +
id +
"/client/" +
clientId +
"/appointments/" +
appointmentId +
"/payments",
title: "Pagos",
icon: <AccountBalanceOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Crear Pago",
icon: <AccountMovementsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
loadAppointmentEvent();
}, []);
const loadAppointmentEvent = () => {
if (!SessionInfo.userId) {
return;
}
if (!appointmentId) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
getAppointmentEvent({
appointmentId: appointmentId.toString(),
sessionUser: SessionInfo.userId,
})
.then((data) => {
setAppointment(data);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
};
const [paymentMethodFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Método de pago:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el nombre del método de pago para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [paymentMethhodFilterData, setPaymentMethodFilterData] = useState<
TextObjectFilterResult[]
>([]);
const [paymentMethodId, setPaymentMethodId] = useState<CLIENT_ACCOUNT_PAYMENT_METHODS>(
CLIENT_ACCOUNT_PAYMENT_METHODS.CASH
);
useEffect(() => {
loadPaymentMethods();
}, []);
const loadPaymentMethods = () => {
setPaymentMethodFilterData(
paymentMethods.map((category) => ({
id: category.id.toString(),
name: category.name,
description: category.description,
}))
);
};
const addPayment = () => {
const data: CreatePaymentParams = {
appointmentId: appointmentId,
method: paymentMethodId,
amount: Number(amount),
description: description,
status: CLIENT_ACCOUNT_MOVEMENT_STATUS.APPROVED,
proccessDate: PAYMENT_PROCCESS_DATE.NOW,
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
createPayment(data)
.then(() => {
alert.showSuccess("Pago creado correctamente");
goTo(
"/admin/org/" +
id +
"/client/" +
clientId +
"/appointments/" +
appointmentId +
"/payments"
);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
});
};
return (
<>
<Location />
<div style={{ marginTop: "15px" }}></div>
{appointment && (
<div style={{ marginTop: "20px" }}>
<AppointmentEventHeader appointment={appointment} showPaymentInfo />
</div>
)}
<TextObjectFilter
placeholder="Método de pago"
content={paymentMethodFilterContent}
data={paymentMethhodFilterData}
onChange={(id) => {
setPaymentMethodId(id as CLIENT_ACCOUNT_PAYMENT_METHODS);
}}
/>
<Textbox
placeholder="Monto"
value={amount}
type="number"
width="100%"
onChange={(e) => setAmount(e.target.value)}
/>
<Textbox
placeholder="Descripción"
value={description}
type="comments"
width="100%"
style={{ height: "100px" }}
limitSize={100}
onChange={(e) => setDescription(e.target.value)}
/>
<Button
text="Agregar"
onClick={() => {
addPayment();
}}
color="primary"
width="100%"
style={{ marginTop: "15px" }}
/>
<div style={{ marginTop: "15px" }}></div>
</>
);
}
@@ -0,0 +1,477 @@
"use client";
import Location from "@core/app/components/Location/Location";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import { useEventHandlerStore, EVENT_TYPES } from "@core/Store/EventHandler.Store";
import { useLocationStore } from "@core/Store/Location.Store";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
import { ThemeProvider } from "@emotion/react";
import turnosXpressTheme from "@core/app/theme/turnosXpress";
import { useSessionStore } from "@core/Store/Sesion.Store";
import { useAlert } from "@core/Store/Alert.Store";
import { useNavigation } from "@core/app/hooks/goto";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { ButtonGroup, DialogActions, DialogContent } from "@mui/material";
import ButtonMaterial from "@mui/material/Button";
import Addable from "@core/app/components/Addable/Addable";
import { getAppointmentEvent } from "../../Appointments.Service";
import { AppointmentEventByClient } from "@core/Models/Appointments.model";
import AppointmentEventHeader from "@core/app/components/AppointmentEventHeader/AppointmentEventHeader";
import { deletePayment, getAppointmentPayments, markAsPayed } from "./AppointmentPayment.Service";
import { IPayment, paymentMethods } from "@core/Models/Payments.model";
import { formatPrice } from "@core/app/helpers/Numbers";
import dayjs from "dayjs";
import { getPaymentIcon } from "@core/app/components/PaymentIcons/PaymentIcons";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
import PaymentToolbar from "@core/app/components/PaymentToolbar/PaymentToolbar";
import DialogWindow from "@core/app/components/DialogWindow/DialogWindow";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import { CLIENT_ACCOUNT_PAYMENT_METHODS } from "@core/Models/ClientAccountMovements.model";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
export type UrlData = {
oid: string;
clientId: string;
appointmentId: string;
};
export default function ClientAppointment() {
const urlData = useParams();
const oid = urlData.oid;
const clientId = urlData.clientId;
const appointmentId = urlData.appointmentId;
const SessionUser = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const alert = useAlert();
const confirm = useConfirmStore();
const { goTo } = useNavigation();
const bottomToolbar = useBottomToolbarStore();
const [dialogPayVisible, setDialogPayVisible] = useState<boolean>(false);
const [appointment, setAppointment] = useState<AppointmentEventByClient | null>(null);
const [appointmentPayments, setAppointmentPayments] = useState<IPayment[]>([]);
const [remainingBalance, setRemainingBalance] = useState<number>(0);
const [paymentMethodFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Método de pago:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el nombre del método de pago para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [paymentMethhodFilterData, setPaymentMethodFilterData] = useState<
TextObjectFilterResult[]
>([]);
const [paymentMethodId, setPaymentMethodId] = useState<CLIENT_ACCOUNT_PAYMENT_METHODS>(
CLIENT_ACCOUNT_PAYMENT_METHODS.CASH
);
useEffect(() => {
loadPaymentMethods();
}, []);
const loadPaymentMethods = () => {
setPaymentMethodFilterData(
paymentMethods.map((category) => ({
id: category.id.toString(),
name: category.name,
description: category.description,
}))
);
};
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + oid + "/clients",
title: "Clientes",
icon: null,
});
location.add({
url: "/admin/org/" + oid + "/client/" + clientId + "/appointments",
title: "Turnos reservados",
icon: <ClientsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Detalles del turno",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
loadAppointmentEvent();
}, []);
useEffect(() => {
calculateRemainingBalance();
}, [appointmentPayments]);
useEffect(() => {
bottomToolbar.set(
<PaymentToolbar
onGotoAppointments={() => {
goTo(
`/admin/org/profile/${oid}/appointments/${encodeURIComponent(
dayjs(appointment?.appointmentDate).clone().startOf("day").toISOString()
)}`
);
}}
onMarkAsPayed={() => {
setDialogPayVisible(true);
}}
/>
);
return () => {
bottomToolbar.clear();
};
}, [appointmentId, appointment]);
const calculateRemainingBalance = () => {
const total = appointment?.price ?? 0;
const paid = appointmentPayments.reduce((acc, payment) => acc + payment.amount, 0);
setRemainingBalance(total - paid);
};
const loadAppointmentEvent = () => {
if (!SessionUser.userId) {
return;
}
if (!appointmentId) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
getAppointmentEvent({
appointmentId: appointmentId.toString(),
sessionUser: SessionUser.userId,
})
.then((data) => {
setAppointment(data);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
loadAppointmentPayments();
});
};
const loadAppointmentPayments = () => {
if (!SessionUser.userId) {
return;
}
if (!appointmentId) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
getAppointmentPayments({
appointmentId: appointmentId.toString(),
sessionUser: SessionUser.userId,
})
.then((data) => {
setAppointmentPayments(data);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
};
const deletePaymentHandler = (paymentId: string) => {
if (!SessionUser.userId) {
return;
}
if (!paymentId) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
deletePayment({
id: paymentId,
sessionUser: SessionUser.userId,
})
.then(() => {
loadAppointmentEvent();
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
};
const markAsPayedHandler = () => {
if (!SessionUser.userId) {
return;
}
if (!appointmentId) {
return;
}
if (appointment?.payment) {
alert.showSuccess("El turno ya está pagado");
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
markAsPayed({
appointmentId: appointmentId.toString(),
method: paymentMethodId,
sessionUser: SessionUser.userId,
})
.then(() => {
loadAppointmentEvent();
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
setDialogPayVisible(false);
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
};
return (
<>
<Location bottomSeperation="70px" />
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "20px",
}}
>
<ThemeProvider theme={turnosXpressTheme}>
<ButtonGroup variant="outlined" aria-label="Basic button group">
<ButtonMaterial
variant="outlined"
onClick={() => {
goTo(
`/admin/org/${oid}/client/${clientId}/appointments/${appointmentId}`
);
}}
>
Información
</ButtonMaterial>
<ButtonMaterial variant="contained">Pagos</ButtonMaterial>
</ButtonGroup>
</ThemeProvider>
</div>
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={`${oid}`}>
<>
<Addable
onClick={() => {
goTo(
`/admin/org/${oid}/client/${clientId}/appointments/${appointmentId}/payments/create`
);
}}
>
{appointment && (
<div style={{ marginTop: "20px" }}>
<AppointmentEventHeader appointment={appointment} showPaymentInfo />
</div>
)}
{appointmentPayments && (
<div style={{ marginTop: "20px" }}>
{appointmentPayments.map((payment, index) => (
<div
key={"payment-" + index}
onClick={() => {
confirm.show(
`¿Estás seguro de querer eliminar este pago de ${formatPrice(
payment.amount
)}?`,
() => {
deletePaymentHandler(payment._id);
},
() => { }
);
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
backgroundColor: "var(--white-dark)",
padding: "15px",
borderRadius: "10px",
marginBottom: "10px",
cursor: "pointer",
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: "10px",
}}
>
<div>{getPaymentIcon(payment.method)}</div>
<div>
<div
style={{
fontSize: "14px",
fontWeight: "bold",
}}
>
{payment.description}
</div>
<div
style={{
fontSize: "12px",
color: "var(--gray-dark)",
}}
>
{dayjs(payment.createdAt).format(
"DD MMMM YYYY HH:mm"
)}
</div>
</div>
</div>
<div
style={{
fontSize: "14px",
fontWeight: "bold",
}}
>
{formatPrice(payment.amount)}
</div>
</div>
</div>
))}
</div>
)}
<div>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
padding: "15px",
borderRadius: "10px",
marginBottom: "10px",
color: "var(--error)",
}}
>
<div>
<div
style={{
fontSize: "14px",
fontWeight: "bold",
}}
>
Saldo restante
</div>
</div>
<div
style={{
fontSize: "14px",
fontWeight: "bold",
}}
>
{formatPrice(remainingBalance)}
</div>
</div>
</div>
<DialogWindow visible={dialogPayVisible} setVisible={setDialogPayVisible}>
<>
<DialogContent
style={{
maxWidth: "500px",
minWidth: "300px",
margin: "auto",
}}
>
<>
<TextObjectFilter
placeholder="Método de pago"
content={paymentMethodFilterContent}
data={paymentMethhodFilterData}
onChange={(id) => {
setPaymentMethodId(
id as CLIENT_ACCOUNT_PAYMENT_METHODS
);
}}
/>
</>
</DialogContent>
<DialogActions>
<ButtonMaterial
variant="contained"
color="primary"
onClick={markAsPayedHandler}
>
Pagar
</ButtonMaterial>
<ButtonMaterial
variant="contained"
color="secondary"
onClick={() => {
setDialogPayVisible(false);
}}
>
Cancelar
</ButtonMaterial>
</DialogActions>
</>
</DialogWindow>
</Addable>
</>
</SecureByPlan>
</>
);
}
@@ -0,0 +1,126 @@
"use client";
import { useAlert } from "@core/Store/Alert.Store";
import {
useEventHandlerStore,
EVENT_TYPES,
} from "@core/Store/EventHandler.Store";
import { useLocationStore } from "@core/Store/Location.Store";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
import { findAppointmentsAdminByClient } from "./Appointments.Service";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import { AppointmentAdminByClientView } from "@core/Models/Appointments.model";
import ServiceItem from "@core/app/components/ServiceItem/ServiceItem";
import Location from "@core/app/components/Location/Location";
import { useNavigation } from "@core/app/hooks/goto";
import { useMenuStore } from "@core/Store/Menu.Store";
type UrlData = {
oid: string;
clientId: string;
};
export default function ClientAppointments() {
const data = useParams<UrlData>();
const id = data.oid;
const clientId = data.clientId;
const alert = useAlert();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const menu = useMenuStore();
const { goTo } = useNavigation();
const [appintments, setAppointments] =
useState<AppointmentAdminByClientView>({
events: [],
});
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
menu.clear();
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
icon: null,
});
location.add({
url: "/admin/org/" + id + "/client/" + clientId + "/profile",
title: "Perfil",
icon: <ClientsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Turnos reservados",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
findAppointmentsAdminByClient({
clientId: clientId,
companyId: id,
})
.then((appointment) => {
setAppointments(appointment);
})
.catch((error) => {
alert.showError(error.message);
});
}, []);
return (
<>
<Location />
<div
style={{
marginTop: "30px",
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(300px, 1fr))",
gap: "20px",
}}
>
{appintments.events.length === 0 ? (
<div style={{
gridColumn: "1 / -1",
textAlign: "center",
padding: "40px 20px",
background: "#fff",
borderRadius: "12px",
boxShadow: "0 4px 15px rgba(0,0,0,0.02)",
color: "#7f8c8d"
}}>
No hay turnos registrados para este cliente.
</div>
) : (
appintments.events.map((event, index) => {
return (
<ServiceItem
key={"appintment." + index}
data={event}
onClick={() => {
goTo(
"/admin/org/" +
id +
"/client/" +
clientId +
"/appointments/" +
event.id
);
}}
/>
);
})
)}
</div>
</>
);
}
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function EditClientLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,195 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import Textbox from "@components/Textbox/Textbox";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import TextPhone from "@core/app/components/TextPhone/TextPhone";
import { updateClient, findClientById } from "../Client.Service";
import { validatePhone } from "@core/helpers/validatePhone";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import Switch from "@core/app/components/Switch/Switch";
import { Box, Typography } from "@mui/material";
type UrlData = {
oid: string;
clientId: string;
};
export default function ClientCreate() {
const data = useParams<UrlData>();
const id = data.oid;
const clientId = data.clientId;
const alert = useAlert();
const confirm = useConfirmStore();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [phoneCountryCode] = useState("549");
const [phoneAreaCode, setPhoneAreaCode] = useState("");
const [phoneNumber, setPhoneNumber] = useState("");
const [banned, setBanned] = useState(false);
const { goTo } = useNavigation();
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
icon: null,
});
location.add({
url: "/admin/org/" + id + "/client/" + clientId + "/profile",
title: "Perfil",
icon: <ClientsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Editar",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
findClientById({
id: clientId,
companyId: id,
sessionUser: SessionInfo.userId,
})
.then((client) => {
setFirstName(client.firstName);
setLastName(client.lastName);
setEmail(client.email);
setPhoneAreaCode(client.phoneAreaCode);
setPhoneNumber(client.phoneNumber);
setBanned(client.banned || false);
})
.catch((error) => {
alert.showError(error.message);
});
}, []);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
const validationPhoneResult = validatePhone(phoneAreaCode, phoneNumber);
if (!validationPhoneResult.isValid) {
confirm.show(
validationPhoneResult.message,
() => {},
() => {}
);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
return;
}
updateClient({
id: clientId,
companyId: id,
firstName: firstName,
lastName: lastName,
email: email,
phoneCountryCode: phoneCountryCode,
phoneAreaCode: phoneAreaCode,
phoneNumber: phoneNumber,
banned: banned,
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess(
`Información del cliente ${firstName} ${lastName} actualizada`
);
setTimeout(() => {
goTo("/admin/org/" + id + "/client/" + clientId + "/profile");
}, 2000);
})
.catch((error) => {
alert.showError(error.message);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
return (
<>
<Location />
<div style={{ marginTop: "15px" }}></div>
<Textbox
placeholder="Nombre"
type="text"
value={firstName}
width="100%"
onChange={(e) => {
setFirstName(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
placeholder="Apellido"
type="text"
value={lastName}
width="100%"
onChange={(e) => {
setLastName(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
placeholder="E-mail"
type="email"
value={email}
width="100%"
onChange={(e) => {
setEmail(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<TextPhone
label="Teléfono"
areaCode={phoneAreaCode}
number={phoneNumber}
width="100%"
onChangeArea={(e) => {
setPhoneAreaCode(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
onChangePhone={(e) => {
setPhoneNumber(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<div style={{ marginTop: "15px" }}></div>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<Typography variant="body1" sx={{ fontWeight: "bold", paddingLeft: "10px" }}>Baneado (No puede reservar)</Typography>
<Switch
checked={banned}
onChange={(checked) => {
setBanned(checked);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Box>
<div style={{ marginTop: "15px" }}></div>
</>
);
}
@@ -0,0 +1,204 @@
"use client";
import { useParams } from "next/navigation";
import style from "./ClientLayout.module.css";
import { useEffect, useState, Suspense } from "react";
import Avatar from "@components/Avatar/Avatar";
import { useSessionStore } from "@store/Sesion.Store";
import SectionContainer from "@components/SectionContainer/SectionContainer";
import { useAlert } from "@store/Alert.Store";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import Secure from "@components/Secure/Secure";
import { deleteClient, findClientById } from "./Client.Service";
import { OrganizationClientView } from "@core/Models/Clients.model";
import SettingsIcon from "@mui/icons-material/Settings";
import IconButton from "@mui/material/IconButton";
import DeleteIcon from "@mui/icons-material/Delete";
import { useMenuStore } from "@core/Store/Menu.Store";
import { Badge, BadgeProps } from "@mui/material";
import Menu from "@core/app/components/Menu/Menu";
import styled from "@emotion/styled";
import Content from "@core/app/components/Content/Content";
import SuspenseLoading from "@core/app/components/SuspendeLoading/SuspenseLoading";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { useNavigation } from "@core/app/hooks/goto";
type UrlData = {
oid: string;
clientId: string;
};
const StyledBadge = styled(Badge)<BadgeProps>(() => ({
"& .MuiBadge-badge": {
right: 5,
top: 13,
padding: "0 4px",
},
}));
export default function ClientDashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const data = useParams<UrlData>();
const oid = data.oid;
const clientId = data.clientId;
const SessionInfo = useSessionStore();
const alert = useAlert();
const confirm = useConfirmStore();
const eventHandler = useEventHandlerStore();
const menu = useMenuStore();
const { goTo } = useNavigation();
const [client, setClient] = useState<OrganizationClientView | null>(null);
const [clientName, setClientName] = useState<string>("");
const [, setClientEmail] = useState<string>("");
const [clientAvatar, setClientAvatar] = useState<string>("");
useEffect(() => {
createMenu();
}, []);
useEffect(() => {
loadClientData();
}, [SessionInfo.userId]);
const createMenu = () => {
menu.clear();
menu.add({
text: "Eliminar Cliente",
onClick: () => {
eliminarCliente();
},
icon: <DeleteIcon />,
});
};
const eliminarCliente = () => {
confirm.show(
"Atención: Al eliminar un cliente se perderá la información relacionada con él, como por ejemplo sus reservas. Estas seguro que quieres eliminar el cliente?",
() => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
deleteClient({
clientId: clientId,
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("El cliente fue eliminado con éxito");
goTo("/admin/org/profile/" + oid + "/clients");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
},
() => {}
);
};
const loadClientData = () => {
if (!SessionInfo.loged) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
findClientById({
id: clientId,
companyId: oid,
sessionUser: SessionInfo.userId,
})
.then((client) => {
setClient(client);
setClientName(client.fullName);
setClientEmail(client.email);
setClientAvatar(client.avatar);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
return (
<>
<Content>
<Suspense fallback={<SuspenseLoading />}>
<Secure>
<SectionContainer style={{ top: "70px" }}>
<div className={style.clientProfileHeader}>
<Avatar
name="userProfileAvatar"
src={clientAvatar}
alt={clientName}
/>
<div>
<h1>{clientName}</h1>
<span>{client?.email}</span>
</div>
</div>
<div
className={style.clientProfileOptionsContainer}
>
{children}
</div>
<div
style={{
position: "absolute",
top: "-40px",
right: "40px",
}}
>
<div className={style.buttonMenu}>
{menu.length() > 0 && (
<StyledBadge
color="warning"
badgeContent="1"
variant="dot"
sx={{
top: "-10px",
}}
>
<IconButton
sx={{ color: "var(--white)" }}
id="button-menu-appointment"
aria-controls={
menu.visible
? "tx-menu"
: undefined
}
aria-expanded={
menu.visible
? "true"
: undefined
}
aria-haspopup="true"
onClick={() => {
menu.setAnchorEl(
document.getElementById(
"button-menu-appointment"
)!
);
menu.show();
}}
>
<SettingsIcon />
</IconButton>
</StyledBadge>
)}
</div>
</div>
<Menu />
</SectionContainer>
</Secure>
</Suspense>
</Content>
</>
);
}
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function EditClientLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,232 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import { TextObjectFilterResult, TOFClientsParams } from "@core/Models/TextObjectFilter.model";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import filterObjects from "@core/app/components/TextObjectFilter/Filter.Service";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import { clientMerge, findClientById } from "../Client.Service";
import { ClientMergeParams, OrganizationClientView } from "@core/Models/Clients.model";
import Button from "@core/app/components/Button/Button";
import MaterialAlert from "@mui/material/Alert";
import { useConfirmStore } from "@core/Store/Confirm.Store";
type UrlData = {
oid: string;
clientId: string;
};
export default function ClientCreate() {
const data = useParams<UrlData>();
const id = data.oid;
const clientId = data.clientId;
const alert = useAlert();
const confirm = useConfirmStore();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const [clientMergeFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Cliente:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del cliente para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [clientMergeFilterData, setClientMergeFilterData] = useState<TextObjectFilterResult[]>(
[]
);
const [clientMergeId, setClientMergeId] = useState<string>("");
const [clientFromData, setClientFromData] = useState<OrganizationClientView | null>(null);
const [clientToData, setClientToData] = useState<OrganizationClientView | null>(null);
const mergeClients = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const clientFrom = clientId;
const clientTo = clientMergeId;
const data: ClientMergeParams = {
sessionUser: SessionInfo.userId,
fromClientId: clientFrom,
toClientId: clientTo,
};
clientMerge(data)
.then(() => {
alert.showSuccess("Los clientes fuerón fusionados con éxito.");
setTimeout(() => {
goTo("/admin/org/" + id + "/client/" + clientTo + "/profile");
}, 3000);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const mergeButtonHandler = () => {
confirm.show(
"Al fusionar los clientes solo se conservará el cliente elegido. ¿Estas seguro que queres fusionar los clientes?",
() => {
mergeClients();
},
() => {}
);
};
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
icon: null,
});
location.add({
url: "/admin/org/" + id + "/client/" + clientId + "/profile",
title: "Perfil",
icon: <ClientsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Fusionar clientes",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
const filter: TOFClientsParams = {
companyId: id,
};
filterObjects<TOFClientsParams>("clients/filter-all", filter).then((res) => {
setClientMergeFilterData(res);
});
}, []);
useEffect(() => {
if (!id) {
return;
}
if (!clientId) {
return;
}
if (!SessionInfo.userId) {
return;
}
findClientById({
id: clientId,
companyId: id,
sessionUser: SessionInfo.userId,
})
.then((client) => {
setClientFromData(client);
})
.catch((error) => {
alert.showError(error.message);
});
}, [id, clientId, SessionInfo.userId]);
useEffect(() => {
if (!id) {
return;
}
if (!clientMergeId) {
return;
}
if (!SessionInfo.userId) {
return;
}
findClientById({
id: clientMergeId,
companyId: id,
sessionUser: SessionInfo.userId,
})
.then((client) => {
setClientToData(client);
})
.catch((error) => {
alert.showError(error.message);
});
}, [id, clientMergeId, SessionInfo.userId]);
return (
<>
<Location />
<div style={{ marginTop: "15px" }}></div>
<TextObjectFilter
placeholder="Elegir cliente"
content={clientMergeFilterContent}
data={clientMergeFilterData}
onChange={(id) => {
setClientMergeId(id);
}}
/>
<MaterialAlert
severity="info"
sx={{
marginTop: "30px",
width: "100%",
marginLeft: "auto",
marginRight: "auto",
}}
>
<strong>Atención:</strong> El cliente seleccionado{" "}
<strong>({clientToData ? clientToData.fullName : "No elegido aún"})</strong> será
fusionado con el cliente actual <strong>({clientFromData?.fullName}).</strong> Esto
significa que todos los turnos del cliente actual serán asignados al cliente
seleccionado. El cliente original será borrado. Solo es posible fusionar un cliente
creado por la organización con un cliente generado por un usuario y prevalecera el
cliente generado por el usuario.
</MaterialAlert>
<Button
onClick={mergeButtonHandler}
text="Fusionar clientes"
style={{ marginTop: "30px" }}
width="100%"
/>
<div style={{ marginTop: "15px" }}></div>
</>
);
}
@@ -0,0 +1,126 @@
"use client";
import ButtonOption from "@components/ButtonOption/ButtonOption";
import Location from "@components/Location/Location";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import { useLocationStore } from "@store/Location.Store";
import { useNavigation } from "@hooks/goto";
import { useParams } from "next/navigation";
import { useEffect } from "react";
type UrlData = {
oid: string;
clientId: string;
};
export default function ClientProfile() {
const data = useParams<UrlData>();
const oid = data.oid;
const clientId = data.clientId;
const location = useLocationStore();
const { goTo } = useNavigation();
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Mis organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + oid,
title: "Organizacion",
icon: (
<StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />
),
});
location.add({
url: "",
title: "Perfil del cliente",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
return (
<>
<Location />
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: "20px",
gap: "10px",
}}
>
<ButtonOption
text="Editar"
icon="/profile-icon.svg"
onClick={() =>
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/edit"
)
}
/>
<ButtonOption
text="Turnos"
icon="/profile-icon.svg"
onClick={() =>
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/appointments"
)
}
/>
<ButtonOption
text="Fusionar Duplicados"
icon="/client-merge-icon.svg"
onClick={() =>
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/merge"
)
}
/>
<ButtonOption
text="Repeticiones"
icon="/repeat-icon.svg"
onClick={() =>
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/repeats"
)
}
/>
<ButtonOption
text="Cuenta"
icon="/account-icon.svg"
onClick={() =>
goTo(
"/admin/org/" +
oid +
"/client/" +
clientId +
"/account"
)
}
/>
</div>
</>
);
}
@@ -0,0 +1,290 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import WeekDaySelector from "@core/app/components/WeekDaySelector/WeekDaySelector";
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
import TextObjectFilter from "@components/TextObjectFilter/TextObjectFilter";
import filterObjects from "@core/app/components/TextObjectFilter/Filter.Service";
import TextObjectFilterItem from "@components/TextObjectFilterItem/TextObjectFilterItem";
import TextTime from "@components/TextTime/TextTime";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import { FindEmployeesParams, FindEmployeesServicesParams } from "@core/Models/Collaborators.model";
import dayjs from "dayjs";
import "dayjs/locale/es";
import Button from "@core/app/components/Button/Button";
import { CreateRepeatsParams } from "@core/Models/Repeats.model";
import { repeatAdd } from "../../Client.Service";
import * as Yup from "yup";
import { ApiError } from "@core/Models/Server.Error.model";
import { FindDiscountsParams } from "@core/Models/Discounts.model";
type UrlData = {
oid: string;
clientId: string;
};
export default function ClientCreate() {
const data = useParams<UrlData>();
const id = data.oid;
const clientId = data.clientId;
const alert = useAlert();
const confirm = useConfirmStore();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const [weekDay, setWeekDay] = useState(1);
const [horario, setHorario] = useState<dayjs.Dayjs>(dayjs(new Date()).hour(8).minute(0));
const { goTo } = useNavigation();
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
icon: null,
});
location.add({
url: "/admin/org/" + id + "/client/" + clientId + "/profile",
title: "Perfil",
icon: <ClientsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Repeticiones",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {}, []);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
console.log("save");
}
}, [eventHandler.eventType]);
const [collaboratorFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Colaborador:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del colaborador para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>(
[]
);
const [collaboratorId, setCollaboratorId] = useState<string>("");
const [serviceFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Servicio:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del servicio para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [serviceFilterData, setServiceFilterData] = useState<TextObjectFilterResult[]>([]);
const [serviceId, setServiceId] = useState<string>("");
useEffect(() => {
const filter: FindEmployeesParams = {
companyId: id,
};
filterObjects<FindEmployeesParams>("employees/filter-all", filter).then((res) => {
setCollaboratorFilterData(res);
});
}, []);
const [discountFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Descuento:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del descuento para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [discountFilterData, setDiscountFilterData] = useState<TextObjectFilterResult[]>([]);
const [discountId, setDiscountId] = useState<string>("");
useEffect(() => {
if (!SessionInfo.userId) return;
const filter: FindDiscountsParams = {
companyId: id,
sessionUser: SessionInfo.userId,
};
filterObjects<FindDiscountsParams>("discounts/filter-all", filter).then((res) => {
setDiscountFilterData(res);
});
}, [SessionInfo.userId]);
useEffect(() => {
setServiceFilterData([]);
setServiceId("");
if (collaboratorId === "") {
return;
}
const filter: FindEmployeesServicesParams = {
companyId: id,
employeeId: collaboratorId,
};
filterObjects<FindEmployeesServicesParams>("employees/services-filter-all", filter).then(
(res) => {
setServiceFilterData(res);
}
);
checkNeedSave();
}, [collaboratorId]);
const weekDayChangeHandler = (day: number) => {
if (eventHandler.eventType === EVENT_TYPES.NEED_SAVE) {
confirm.show(
"Si no guardas los cambios se perderán. Estas seguro que queres cambiar el dia?",
() => {
setWeekDay(day);
},
() => {
setWeekDay(weekDay);
}
);
} else {
setWeekDay(day);
}
};
const checkNeedSave = () => {
if (collaboratorId !== "" && clientId !== "" && serviceId !== "") {
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}
};
const handleReservar = () => {
const data: CreateRepeatsParams = {
companyId: id,
employeeId: collaboratorId,
serviceId: serviceId,
clientId: clientId,
discountId: discountId,
weekDay: weekDay,
startHour: horario.hour() + horario.minute() / 60,
sessionUser: SessionInfo.userId,
};
repeatAdd(data)
.then(() => {
goTo("/admin/org/" + id + "/client/" + clientId + "/repeats");
})
.catch((error) => {
if (error instanceof Yup.ValidationError) {
alert.showError(error.message);
} else if (error instanceof ApiError) {
alert.showError(error.format());
}
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
return (
<>
<Location />
<HeaderConfProvider />
<div style={{ marginTop: "30px" }}>
<WeekDaySelector value={weekDay} onChange={weekDayChangeHandler} />
<TextObjectFilter
placeholder="Colaborador"
content={collaboratorFilterContent}
data={collaboratorFilterData}
onChange={(id) => {
setCollaboratorId(id);
checkNeedSave();
}}
/>
<TextObjectFilter
placeholder="Servicio"
content={serviceFilterContent}
data={serviceFilterData}
onChange={(id) => {
setServiceId(id);
checkNeedSave();
}}
selectedId={serviceId}
/>
<TextObjectFilter
placeholder="Descuento"
content={discountFilterContent}
data={discountFilterData}
onChange={(id) => {
setDiscountId(id);
checkNeedSave();
}}
selectedId={discountId}
/>
<TextTime
placeholder="Horario"
width="100%"
value={horario}
onChange={setHorario}
/>
<Button
text="Reservar"
width="100%"
color="primary"
style={{ height: "42px", marginTop: "20px" }}
onClick={handleReservar}
/>
</div>
<div style={{ marginTop: "15px" }}></div>
</>
);
}
@@ -0,0 +1,30 @@
"use client";
import Addable from "@core/app/components/Addable/Addable";
import { useNavigation } from "@core/app/hooks/goto";
import { EVENT_TYPES, useEventHandlerStore } from "@core/Store/EventHandler.Store";
import { useParams } from "next/navigation";
type UrlData = {
oid: string;
clientId: string;
};
export default function EditClientLayout({ children }: { children: React.ReactNode }) {
const urlData = useParams<UrlData>();
//const oid = urlData.oid;
const clientId = urlData.clientId;
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const handleAdd = () => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
goTo("/admin/org/" + urlData.oid + "/client/" + clientId + "/repeats/create");
};
return (
<>
<Addable onClick={handleAdd}>{children}</Addable>
</>
);
}
@@ -0,0 +1,179 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import IconButton from "@mui/material/IconButton";
import DeleteIcon from "@mui/icons-material/Delete";
import { useAlert } from "@store/Alert.Store";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import HeaderConfProvider from "@core/app/components/HeaderConfProvider/HeaderConfProvider";
import { deleteRepeat, getRepeatsByClient } from "../Client.Service";
import { DeleteRepeatsParams, FindRepeatsParams, RepeatView } from "@core/Models/Repeats.model";
import style from "./repeats.module.css";
type UrlData = {
oid: string;
clientId: string;
};
export default function ClientCreate() {
const data = useParams<UrlData>();
const id = data.oid;
const clientId = data.clientId;
const alert = useAlert();
const confirm = useConfirmStore();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const [repeats, setRepeats] = useState<RepeatView[]>([]);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
if (!id) {
return;
}
if (!clientId) {
return;
}
loadRepeats();
}, [id, clientId]);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
icon: null,
});
location.add({
url: "/admin/org/" + id + "/client/" + clientId + "/profile",
title: "Perfil",
icon: <ClientsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Repeticiones",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
}, []);
const loadRepeats = () => {
const data: FindRepeatsParams = {
companyId: id,
clientId: clientId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
getRepeatsByClient(data)
.then((repeats) => {
setRepeats(repeats);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
};
const handleDelete = (id: string) => {
if (!SessionInfo.userId) {
return;
}
if (!id) {
return;
}
const data: DeleteRepeatsParams = {
repeatId: id,
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
deleteRepeat(data)
.then(() => {
loadRepeats();
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
};
return (
<>
<Location />
<HeaderConfProvider />
<div style={{ marginTop: "15px" }}></div>
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
marginTop: "20px",
}}
>
{repeats.map((repeat) => {
return (
<div
key={repeat.id}
className={style.cardRepeat}
style={{
backgroundColor: repeat.employeeCalendarColor,
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
}}
>
<div>
<h3>
Todos los {repeat.weekDayName} a las {repeat.startHourFormatted}
</h3>
<h1>{repeat.serviceName}</h1>
<h2>con {repeat.employeeName}</h2>
</div>
<div>
<IconButton
aria-label="delete"
sx={{ color: "var(--white)" }}
onClick={() => {
confirm.show(
"Estas seguro que queres eliminar la repeticion?",
() => {
handleDelete(repeat.id);
},
() => {}
);
}}
>
<DeleteIcon />
</IconButton>
</div>
</div>
);
})}
</div>
<div style={{ marginTop: "15px" }}></div>
</>
);
}
@@ -0,0 +1,39 @@
.cardRepeat {
margin-top: 10px;
border-radius: 6px;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 15px;
padding-right: 15px;
cursor: pointer;
-webkit-box-shadow: 0px 2px 8px -4px var(--wine-superdark);
-moz-box-shadow: 0px 2px 8px -4px var(--wine-superdark);
box-shadow: 0px 2px 8px -4px var(--wine-superdark);
margin-left: 2px;
margin-right: 2px;
}
.cardRepeat h1 {
font-size: 18px;
color: var(--white);
text-shadow: 2px 2px 2px var(--black);
}
.cardRepeat h2 {
font-size: 15px;
color: var(--white);
text-shadow: 2px 2px 2px var(--black);
}
.cardRepeat h3 {
font-size: 13px;
color: var(--white);
text-shadow: 2px 2px 2px var(--black);
}
.cardRepeat p {
font-size: 15px;
color: var(--white);
margin-bottom: 5px;
text-shadow: 2px 2px 2px var(--black);
}
@@ -0,0 +1,107 @@
import {
MyOranizationsView,
UpdateCompanyParams,
OranizationsViewByIdParams,
SetOrganizationPublishedStatusParams,
} from "@models/Company.model";
import { ApiVoidResult } from "@models/Api.VoidResult.type";
import ApiRequest from "@services/Api.Service";
import {
EnableScheduleParams,
FindSchedulesEnabledParams,
SchedulesEnabledView,
} from "@models/SchedulesEnabled.type";
import {
CreatePaymentParams,
DeletePaymentParams,
FindPaymentsParams,
IPayment,
PaginateNotPayedAppointmentsParams,
PaginateNotPayedAppointmentsResult,
PaginatePaymentsViewParams,
PaginatePaymentsViewResults,
NotPayedReportResult,
PaymentsReportResult,
} from "@core/Models/Payments.model";
export const savePublishedStatus = async (
data: SetOrganizationPublishedStatusParams
): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("companies/set-published-status", data);
};
export const loadOrganizationsById = async (
data: OranizationsViewByIdParams
): Promise<MyOranizationsView> => {
return ApiRequest.post<MyOranizationsView>("companies/get-by-id", data);
};
export const updateOrganizationsById = async (
data: UpdateCompanyParams
): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("companies/update", data);
};
export const loadSchedulesEnabled = async (
data: FindSchedulesEnabledParams
): Promise<SchedulesEnabledView[]> => {
return ApiRequest.post<SchedulesEnabledView[]>("schedules-enabled/find", data);
};
export const enableSchedule = async (data: EnableScheduleParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("schedules-enabled/enable", data);
};
export const disableSchedule = async (data: EnableScheduleParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("schedules-enabled/disable", data);
};
export const uploadHeaderImage = async (data: FormData): Promise<ApiVoidResult> => {
return ApiRequest.postFile<ApiVoidResult>("companies/upload-header", data);
};
export const loadPaymentsViewTotal = async (data: FindPaymentsParams): Promise<number> => {
return ApiRequest.post<number>("payments/paginated-view-total", data);
};
export const loadPaymentsView = async (
data: PaginatePaymentsViewParams
): Promise<PaginatePaymentsViewResults> => {
return ApiRequest.post<PaginatePaymentsViewResults>("payments/paginate-view", data);
};
export const paginateNotPayedAppointments = async (
data: PaginateNotPayedAppointmentsParams
): Promise<PaginateNotPayedAppointmentsResult> => {
return ApiRequest.post<PaginateNotPayedAppointmentsResult>("payments/not-payed", data);
};
export const paginateNotPayedAppointmentsTotal = async (
data: FindPaymentsParams
): Promise<number> => {
return ApiRequest.post<number>("payments/not-payed-total", data);
};
export const payNotPayedAppointments = async (data: FindPaymentsParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("payments/pay-not-payed", data);
};
export const GetNotPayedReport = async (
data: FindPaymentsParams
): Promise<NotPayedReportResult> => {
return ApiRequest.post<NotPayedReportResult>("payments/get-not-payed-report", data);
};
export const deletePayment = async (data: DeletePaymentParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("payments/delete", data);
};
export const createPayment = async (data: CreatePaymentParams): Promise<IPayment> => {
return ApiRequest.post<IPayment>("payments/create", data);
};
export const GetPaymentsReport = async (
data: FindPaymentsParams
): Promise<PaymentsReportResult> => {
return ApiRequest.post<PaymentsReportResult>("payments/get-payments-report", data);
};
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function OrgConfigLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,415 @@
"use client";
import { useParams } from "next/navigation";
import { useState, useEffect } from "react";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useAlert } from "@store/Alert.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import OrgConfigIcon from "@mui/icons-material/SettingsOutlined";
import Textbox from "@components/Textbox/Textbox";
import { Grid2 } from "@mui/material";
import { loadOrganizationsById, updateOrganizationsById } from "../Org.Service";
import { MyOranizationsView } from "@models/Company.model";
import { ApiError } from "@models/Server.Error.model";
import { TextObjectModalContent } from "@store/TextObjectModal.Store";
import { TextObjectFilterResult } from "@models/TextObjectFilter.model";
import TextObjectFilterItem from "@components/TextObjectFilterItem/TextObjectFilterItem";
import filterObjects from "@components/TextObjectFilter/Filter.Service";
import {
FindLocalidadesParams,
FindPartidosParams,
FindProvinciasParams,
} from "@models/Location.model";
import TextObjectFilter from "@components/TextObjectFilter/TextObjectFilter";
import SelectLocationMap, {
LocationCoordinates,
} from "@core/app/components/SelectLocationMap/SelectLocationMap";
type UrlData = {
id: string;
};
export default function OrganizationEdit() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
const [street, setStreet] = useState("");
const [streetNumber, setStreetNumber] = useState("");
const [builingFloor, setBuilingFloor] = useState("");
const [buildingApartament, setBuildingApartament] = useState("");
const [block, setBlock] = useState("");
const [city, setCity] = useState("");
const [state, setState] = useState("");
const [country, setCountry] = useState("");
const [zipCode, setZipCode] = useState("");
const [latitude, setLatitude] = useState<number>(0);
const [longitude, setLongitude] = useState<number>(0);
const [initialLatitude, setInitialLatitude] = useState<number>(0);
const [initialLongitude, setInitialLongitude] = useState<number>(0);
/**
* FILTRO DE PROVINCIA
* ===================
* */
const [provinciaFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Provincia:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre de la provincia para buscarla.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem
key={index}
data={result}
style={{ textTransform: "capitalize" }}
/>
),
});
const [provinciaFilterData, setProvinciaFilterData] = useState<TextObjectFilterResult[]>([]);
const [selectedProvincia, setSelectedProvincia] = useState<string>("");
/**
* FILTRO DE PARTIDO
* =================
* */
const [partidoFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Partido:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del partido para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem
key={index}
data={result}
style={{ textTransform: "capitalize" }}
/>
),
});
const [partidosFilterData, setPartidosFilterData] = useState<TextObjectFilterResult[]>([]);
const [selectedPartido, setSelectedPartido] = useState<string>("");
/**
* FILTRO DE LOCALIDAD
* ===================
* */
const [localidadFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Localidad:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre de la localidad para buscarla.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem
key={index}
data={result}
style={{ textTransform: "capitalize" }}
/>
),
});
const [localidadesFilterData, setLocalidadesFilterData] = useState<TextObjectFilterResult[]>(
[]
);
const [selectedLocalidad, setSelectedLocalidad] = useState<string>("");
const cargarProvincias = () => {
filterObjects<FindProvinciasParams>("location/provincias-filter-all", {}).then((res) => {
setProvinciaFilterData(res);
});
};
const cargarPartidos = (provincia: string) => {
filterObjects<FindPartidosParams>("location/partidos-filter-all", {
provincia,
}).then((res) => {
setPartidosFilterData(res);
});
};
const cargarLocalidades = (provincia: string, partido: string) => {
filterObjects<FindLocalidadesParams>("location/localidades-filter-all", {
provincia,
partido,
}).then((res) => {
setLocalidadesFilterData(res);
});
};
const handleLocationChange = (location: LocationCoordinates) => {
setLatitude(location.latitude);
setLongitude(location.longitude);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
};
useEffect(() => {
setSelectedProvincia(state);
}, [provinciaFilterData]);
useEffect(() => {
setSelectedPartido(city);
}, [partidosFilterData]);
useEffect(() => {
setSelectedLocalidad(block);
}, [localidadesFilterData]);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Perfil",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Ubicación",
icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
if (!SessionInfo.userId) return;
loadOrganizationsById({ id: id, sessionUser: SessionInfo.userId })
.then((response: MyOranizationsView) => {
setStreet(response.street);
setStreetNumber(response.streetNumber);
setBuilingFloor(response.builingFloor);
setBuildingApartament(response.buildingApartament);
setBlock(response.block.toLocaleUpperCase());
setCity(response.city.toLocaleUpperCase());
setState(response.state.toLocaleUpperCase());
setCountry(response.country);
setZipCode(response.zipCode);
setInitialLatitude(response.latitude);
setInitialLongitude(response.longitude);
cargarProvincias();
cargarPartidos(response.state.toUpperCase());
cargarLocalidades(response.state.toUpperCase(), response.city.toUpperCase());
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}, [SessionInfo.userId]);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const data = {
id: id,
street: street,
streetNumber: streetNumber,
builingFloor: builingFloor,
buildingApartament: buildingApartament,
block: block,
city: city,
state: state,
country: country,
zipCode: zipCode,
latitude: latitude,
longitude: longitude,
sessionUser: SessionInfo.userId,
};
updateOrganizationsById(data)
.then(() => {
alert.showSuccess("Organizacion actualizada!");
})
.catch((error: ApiError) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
return (
<>
<Location />
<Grid2 container sx={{ marginTop: "10px" }}>
<Grid2 size={6}>
<Textbox
name="street"
placeholder="Calle"
type="text"
value={street}
width="100%"
onChange={(e) => {
setStreet(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
<Grid2 size={1}></Grid2>
<Grid2 size={5}>
<Textbox
name="streetNumber"
placeholder="Nro."
type="text"
value={streetNumber}
width="100%"
onChange={(e) => {
setStreetNumber(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
</Grid2>
<Grid2 container>
<Grid2 size={6}>
<Textbox
name="buildingApartament"
placeholder="Apartamento"
type="text"
value={buildingApartament}
width="100%"
onChange={(e) => {
setBuildingApartament(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
<Grid2 size={1}></Grid2>
<Grid2 size={5}>
<Textbox
name="builingFloor"
placeholder="Piso"
type="text"
value={builingFloor}
width="100%"
onChange={(e) => {
setBuilingFloor(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
</Grid2>
{/* <Textbox
name="country"
placeholder="Pais"
type="text"
value={country}
width="100%"
onChange={(e) => {
setCountry(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/> */}
<TextObjectFilter
placeholder="Provincia"
content={provinciaFilterContent}
data={provinciaFilterData}
selectedId={selectedProvincia}
onChange={(id) => {
setState(id);
if (id !== "") {
cargarPartidos(id);
} else {
setPartidosFilterData([]);
}
setCity("");
setLocalidadesFilterData([]);
setBlock("");
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<TextObjectFilter
placeholder="Partido"
content={partidoFilterContent}
data={partidosFilterData}
selectedId={selectedPartido}
onChange={(id) => {
setCity(id);
if (id !== "") {
cargarLocalidades(state, id);
} else {
setLocalidadesFilterData([]);
}
setBlock(" ");
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<TextObjectFilter
placeholder="Localidad"
content={localidadFilterContent}
data={localidadesFilterData}
selectedId={selectedLocalidad}
onChange={(id) => {
setBlock(id);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
name="zipCode"
placeholder="C.P."
type="text"
value={zipCode}
width="100%"
onChange={(e) => {
setZipCode(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<SelectLocationMap
onChange={handleLocationChange}
initialCoordinates={{ latitude: initialLatitude, longitude: initialLongitude }}
/>
<div style={{ marginBottom: "40px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,44 @@
import { ApiVoidResult } from "@core/Models/Api.VoidResult.type";
import { DeleteCompanyParams } from "@core/Models/Company.model";
import ApiRequest from "@services/Api.Service";
import * as Yup from "yup";
const schema = Yup.object().shape({
companyId: Yup.string().required("No se ha especificado una organización."),
sessionUser: Yup.string().required(
"No se ha podido determinar el usuario que intenta realizar esta acción"
),
});
interface ValidateErrorResult {
status: boolean;
error?: Yup.ValidationError;
}
const validateData = (data: DeleteCompanyParams): ValidateErrorResult => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
try {
schema.validate(data, { abortEarly: true });
return {
status: true,
};
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return {
status: false,
error: yupErrors,
};
}
};
export const deleteOrganization = async (data: DeleteCompanyParams): Promise<ApiVoidResult> => {
const validation = validateData(data);
if (validation.status == false) {
throw new Error(validation.error?.message);
}
return ApiRequest.post<ApiVoidResult>("companies/delete", data);
};
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function OrgConfigLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,129 @@
"use client";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useAlert } from "@store/Alert.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import OrgConfigIcon from "@mui/icons-material/SettingsOutlined";
import MaterialAlert from "@mui/material/Alert";
import Textbox from "@components/Textbox/Textbox";
import Button from "@core/app/components/Button/Button";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { deleteOrganization } from "./advanced.Service";
import { useNavigation } from "@core/app/hooks/goto";
type UrlData = {
id: string;
};
export default function OrganizationAdvancedEdit() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
const confirm = useConfirmStore();
const [verification, setVerification] = useState("");
const { goTo } = useNavigation();
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Perfil",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/wap/" + id,
title: "Avanzado",
icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
const eliminarHandler = () => {
if (verification === "eliminar-org") {
confirm.show(
"Si eliminas una organización se perderán todos los datos relacionados con ella. ¿Estas seguro que queres eliminar la organización?",
() => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
deleteOrganization({ companyId: id, sessionUser: SessionInfo.userId })
.then(() => {
goTo("/admin/org");
alert.showSuccess("Organización eliminada con éxito");
})
.catch((error) => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
},
() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}
);
} else {
alert.showError("La confirmación no es correcta");
}
};
return (
<>
<Location />
<div
style={{
marginTop: "20px",
display: "flex",
flexDirection: "column",
justifyContent: "start",
alignItems: "center",
}}
>
<MaterialAlert severity="info" sx={{ marginTop: "20px", width: "100%" }}>
<p>
<strong>Atención:</strong> las acciones realizadas en esta sección son
irreversibles, es decir, que no se pueden deshacer. Si decide eliminar la
organización, se perderán todos los datos relacionados con ella.
</p>
<p>
<strong>Importante:</strong> para dar de baja la organización escribe{" "}
<strong>eliminar-org</strong> en la casilla de confirmación.
</p>
</MaterialAlert>
<Textbox
name="verification"
placeholder="Confirmación"
type="text"
value={verification}
width="100%"
onChange={(e) => {
setVerification(e.target.value);
}}
/>
<Button
text="Eliminar organización"
width="100%"
color="error"
style={{ height: "42px", marginTop: "10px" }}
onClick={eliminarHandler}
/>
</div>
<div style={{ marginBottom: "40px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,54 @@
import ApiRequest from "@services/Api.Service";
import {
IAppointment,
CreateAppointmentParams,
FindAppointmentsParams,
AppointmentAdminByDateView,
} from "@models/Appointments.model";
import * as Yup from "yup";
import { AddRepeatDisabledDateParams } from "@core/Models/Repeats.model";
import { ApiVoidResult } from "@core/Models/Api.VoidResult.type";
const schemaAppointmentAdd = Yup.object().shape({
companyId: Yup.string().required("Debe seleccionar una organización."),
serviceId: Yup.string().required("Debe seleccionar un servicio."),
employeeId: Yup.string().required("Debe seleccionar un colaborador."),
clientId: Yup.string().required("Debe seleccionar un cliente."),
start: Yup.string().required("Debe seleccionar una fecha."),
sessionUser: Yup.string().required("Para crear un turno debe iniciar sesión."),
});
export const createAppointment = async (data: CreateAppointmentParams): Promise<IAppointment> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaAppointmentAdd.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new Error(yupErrors.message);
}
return ApiRequest.post<IAppointment>("appointments/create", data);
};
export const findAppointmentsAdminByDate = async (
data: FindAppointmentsParams
): Promise<AppointmentAdminByDateView> => {
return ApiRequest.post<AppointmentAdminByDateView>("appointments/find-admin-by-date", data);
};
export const addDisabledRepeatDate = async (
data: AddRepeatDisabledDateParams
): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("repeats/add-disabled-date", data);
};
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function CreateClientLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,323 @@
"use client";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import TextObjectFilter from "@components/TextObjectFilter/TextObjectFilter";
import { TextObjectModalContent } from "@store/TextObjectModal.Store";
import { TOFClientsParams, TextObjectFilterResult } from "@models/TextObjectFilter.model";
import filterObjects from "@core/app/components/TextObjectFilter/Filter.Service";
import TextObjectFilterItem from "@components/TextObjectFilterItem/TextObjectFilterItem";
import { FindEmployeesParams, FindEmployeesServicesParams } from "@core/Models/Collaborators.model";
import TextTime from "@components/TextTime/TextTime";
import dayjs from "dayjs";
import "dayjs/locale/es";
import { CreateAppointmentParams } from "@core/Models/Appointments.model";
import { useSessionStore } from "@store/Sesion.Store";
import { createAppointment } from "../Appointment.Service";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import Switch from "@mui/material/Switch";
import { ThemeProvider } from "@emotion/react";
import turnosXpressTheme from "@core/app/theme/turnosXpress";
dayjs.locale("es");
type UrlData = {
id: string;
appdate: string;
};
export default function ClientCreate() {
const data = useParams<UrlData>();
const id = data.id;
const appointmentDate = dayjs(decodeURIComponent(data.appdate));
const SessionUser = useSessionStore();
const alert = useAlert();
const { goTo } = useNavigation();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const [validation, setValidation] = useState<boolean>(true);
const [notification, setNotification] = useState<boolean>(true);
const [clientFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Cliente:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del cliente para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [clientFilterData, setClientFilterData] = useState<TextObjectFilterResult[]>([]);
const [clientId, setClientId] = useState<string>("");
const [collaboratorFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Colaborador:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del colaborador para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>(
[]
);
const [collaboratorId, setCollaboratorId] = useState<string>("");
const [serviceFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Servicio:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del servicio para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [serviceFilterData, setServiceFilterData] = useState<TextObjectFilterResult[]>([]);
const [serviceId, setServiceId] = useState<string>("");
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
const filter: TOFClientsParams = {
companyId: id,
};
filterObjects<TOFClientsParams>("clients/filter-all", filter).then((res) => {
setClientFilterData(res);
});
}, []);
useEffect(() => {
const filter: FindEmployeesParams = {
companyId: id,
};
filterObjects<FindEmployeesParams>("employees/filter-all", filter).then((res) => {
setCollaboratorFilterData(res);
});
}, []);
useEffect(() => {
setServiceFilterData([]);
setServiceId("");
if (collaboratorId === "") {
return;
}
const filter: FindEmployeesServicesParams = {
companyId: id,
employeeId: collaboratorId,
};
filterObjects<FindEmployeesServicesParams>("employees/services-filter-all", filter).then(
(res) => {
setServiceFilterData(res);
}
);
checkNeedSave();
}, [collaboratorId]);
const [horario, setHorario] = useState<dayjs.Dayjs>(dayjs(new Date()).hour(8).minute(0));
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id,
title: "Origanizacion",
icon: null,
});
location.add({
url:
"/admin/org/profile/" +
id +
"/appointments/" +
encodeURIComponent(dayjs(appointmentDate).clone().startOf("day").toISOString()),
title: "Turnos",
icon: <ClientsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Crear nuevo turno",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
const addData: CreateAppointmentParams = {
companyId: id,
serviceId: serviceId,
clientId: clientId,
employeeId: collaboratorId,
start: appointmentDate.hour(horario.hour()).minute(horario.minute()).toISOString(),
validation: validation,
notification: notification,
sessionUser: SessionUser.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
createAppointment(addData)
.then(() => {
alert.showSuccess("Turno creado con exito");
goTo(
"/admin/org/profile/" +
id +
"/appointments/" +
encodeURIComponent(
dayjs(appointmentDate).clone().startOf("day").toISOString()
)
);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
useEffect(() => {
checkNeedSave();
}, [collaboratorId, clientId, serviceId]);
const checkNeedSave = () => {
if (collaboratorId !== "" && clientId !== "" && serviceId !== "") {
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}
};
return (
<>
<Location />
<div style={{ marginTop: "15px" }}>
Nuevo turno para el dia{" "}
<strong>{appointmentDate.format("dddd DD [de] MMMM, YYYY")}</strong>
</div>
<TextObjectFilter
placeholder="Cliente"
content={clientFilterContent}
data={clientFilterData}
onChange={(id) => {
setClientId(id);
checkNeedSave();
}}
/>
<TextObjectFilter
placeholder="Colaborador"
content={collaboratorFilterContent}
data={collaboratorFilterData}
onChange={(id) => {
setCollaboratorId(id);
checkNeedSave();
}}
/>
<TextObjectFilter
placeholder="Servicio"
content={serviceFilterContent}
data={serviceFilterData}
onChange={(id) => {
setServiceId(id);
checkNeedSave();
}}
selectedId={serviceId}
/>
<TextTime placeholder="Desde" width="100%" value={horario} onChange={setHorario} />
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
marginTop: "10px",
}}
>
<ThemeProvider theme={turnosXpressTheme}>
<Switch
checked={validation}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setValidation(event.target.checked);
}}
size="medium"
/>
</ThemeProvider>
<span
style={{
fontSize: "16px",
fontWeight: "600",
color: "var(--black)",
}}
>
Validar disponibilidad?
</span>
</div>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
marginTop: "10px",
}}
>
<ThemeProvider theme={turnosXpressTheme}>
<Switch
checked={notification}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setNotification(event.target.checked);
}}
size="medium"
/>
</ThemeProvider>
<span
style={{
fontSize: "16px",
fontWeight: "600",
color: "var(--black)",
}}
>
Enviar notificatión?
</span>
</div>
<div style={{ marginTop: "35px" }}></div>
</>
);
}
@@ -0,0 +1,4 @@
"use client";
export default function AppointmentLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
@@ -0,0 +1,70 @@
.appointmentsContent {
display: flex;
flex-direction: column;
margin-top: 10px;
position: absolute;
left: 0px;
right: 0px;
top: 85px;
bottom: 10px;
overflow-x: hidden;
overflow-y: auto;
scrollbar-width: none;
}
.appointmentsTitle {
display: flex;
flex-direction: column;
margin-top: 10px;
position: absolute;
left: 0px;
right: 0px;
top: 55px;
text-transform: capitalize;
padding-left: 5px;
}
.buttonPaste {
position: fixed;
display: flex;
align-items: center;
justify-content: center;
bottom: 75px;
right: 15px;
background-color: var(--wine-dark);
color: var(--white);
border-radius: 50%;
z-index: 2000;
cursor: pointer;
width: 64px;
height: 64px;
animation: buttonPastePulseShadow 1.5s ease-in-out infinite;
}
@keyframes buttonPastePulseShadow {
0%,
100% {
box-shadow: 0px 2px 12px -4px var(--wine-red);
}
50% {
box-shadow: 0px 2px 20px -2px var(--wine-red);
}
}
.buttonCancelPaste {
position: fixed;
display: flex;
align-items: center;
justify-content: center;
bottom: 150px;
right: 23px;
background-color: var(--gray-dark);
color: var(--white);
border-radius: 50%;
z-index: 2000;
cursor: pointer;
width: 48px;
height: 48px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.4);
animation: buttonPastePulseShadow 1.5s ease-in-out infinite;
}
@@ -0,0 +1,617 @@
"use client";
import style from "./page.module.css";
import { useState, useEffect, useRef } from "react";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import HomeIcon from "@mui/icons-material/Home";
import IconButton from "@mui/material/IconButton";
import ArrowLeftIcon from "@mui/icons-material/ArrowLeft";
import ArrowRightIcon from "@mui/icons-material/ArrowRight";
import FastRewindIcon from "@mui/icons-material/FastRewind";
import FastForwardIcon from "@mui/icons-material/FastForward";
import Addable from "@components/Addable/Addable";
import { useNavigation } from "@hooks/goto";
import { DaySchedule, ScheduleView } from "react-schedule-view";
import { scViewTurnosExpress, TXCalendarEvent } from "@themes/scheduleView";
import { useBottomToolbarStore } from "@store/BottomToolbar.Store";
import {
addDisabledRepeatDate,
createAppointment,
findAppointmentsAdminByDate,
} from "./Appointment.Service";
import { CreateAppointmentParams, FindAppointmentsParams } from "@core/Models/Appointments.model";
import { useAlert } from "@core/Store/Alert.Store";
import MaterialAlert from "@mui/material/Alert";
import filterObjects from "@components/TextObjectFilter/Filter.Service";
import dayjs, { ManipulateType } from "dayjs";
import "dayjs/locale/es";
import Filterable from "@core/app/components/Filterable/Filterable";
import { useFilterStore } from "@core/Store/Filter.Store";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import { FindEmployeesParams } from "@core/Models/Collaborators.model";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import { useAppointmentsViewStore } from "@core/Store/AppointmentsView.Store";
import ActionDialog from "@core/app/components/ActionDialog/ActionDialog";
import { Button } from "@mui/material";
import { useSessionStore } from "@core/Store/Sesion.Store";
import { AddRepeatDisabledDateParams } from "@core/Models/Repeats.model";
import CalendarRowView from "@core/app/components/CalendarRowView/CalendarRowView";
import useWindowSize from "@core/app/hooks/WindowSize";
import ContentPasteIcon from "@mui/icons-material/ContentPaste";
import CloseIcon from "@mui/icons-material/Close";
import { useClipboardStore } from "@store/Clipboard.Store";
import { useTimePickerStore } from "@store/TimePicker.Store";
import ApiRequest from "@services/Api.Service";
dayjs.locale("es");
type UrlData = {
id: string;
appdate: string;
};
enum CalendarViewMode {
ROW = "row",
COLUMN = "column",
}
interface FilterContentProps {
data: TextObjectFilterResult[];
setCollaboratorId: (id: string) => void;
}
const FilterContent = (props: FilterContentProps) => {
//const filter = useFilterStore();
const appoinmentsView = useAppointmentsViewStore();
//const onChangeHandler = (field: string, value: string) => {};
const [collaboratorFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Colaborador:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el nombre del colaborador para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
return (
<>
{!appoinmentsView.collaboratorId && (
<TextObjectFilter
placeholder="Colaborador"
content={collaboratorFilterContent}
data={props.data}
onChange={(id) => {
props.setCollaboratorId(id);
}}
/>
)}
</>
);
};
export interface AppointmentToolbarProps {
onNext: (mode: ManipulateType) => void;
onPrevious: (mode: ManipulateType) => void;
onHome: () => void;
}
const AppointmentsToolbar = (props: AppointmentToolbarProps) => {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
height: "60px",
color: "var(--white)",
}}
>
<IconButton
color="inherit"
onClick={() => {
props.onPrevious("week");
}}
>
<FastRewindIcon sx={{ ml: 1, mr: 1 }} />
</IconButton>
<IconButton
color="inherit"
onClick={() => {
props.onPrevious("day");
}}
>
<ArrowLeftIcon sx={{ ml: 1, mr: 1 }} />
</IconButton>
<IconButton
color="inherit"
onClick={() => {
props.onHome();
}}
>
<HomeIcon sx={{ ml: 1, mr: 1 }} />
</IconButton>
<IconButton
color="inherit"
onClick={() => {
props.onNext("day");
}}
>
<ArrowRightIcon sx={{ ml: 1, mr: 1 }} />
</IconButton>
<IconButton
color="inherit"
onClick={() => {
props.onNext("week");
}}
>
<FastForwardIcon sx={{ ml: 1, mr: 1 }} />
</IconButton>
</div>
);
};
export default function OrganizationProfile() {
const data = useParams<UrlData>();
const id = data.id;
const appointmentDate = data.appdate
? dayjs(decodeURIComponent(data.appdate))
: dayjs(new Date());
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const filter = useFilterStore();
const alert = useAlert();
const SessionInfo = useSessionStore();
const appointmentsView = useAppointmentsViewStore();
const { goTo } = useNavigation();
const [windowWidth] = useWindowSize();
const bottomToolbar = useBottomToolbarStore();
const [viewMode, setViewMode] = useState<CalendarViewMode>(CalendarViewMode.ROW);
const [currentDate, setCurrentDate] = useState<dayjs.Dayjs>(appointmentDate);
const [calendarEventsCount, setCalendarEventsCount] = useState<number>(0);
const [calendarFrom, setCalendarFrom] = useState<number>(8);
const [calendarTo, setCalendarTo] = useState<number>(23);
const [calendarData, setCalendarData] = useState<DaySchedule<TXCalendarEvent>[]>([
{
name: "",
events: [],
},
]);
const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>(
[]
);
const [collaboratorId, setCollaboratorId] = useState<string>("");
const [dialogVisible, setDialogVisible] = useState<boolean>(false);
const [dialogCalendarEvent, setDialogCalendarEvent] = useState<TXCalendarEvent | undefined>(
undefined
);
const timePicker = useTimePickerStore();
const copiedAppointment = useClipboardStore(state => state.copiedAppointment);
const setCopiedAppointment = useClipboardStore(state => state.setCopiedAppointment);
useEffect(() => {
if (copiedAppointment) {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
} else {
eventHandler.setEventType(copiedAppointment ? EVENT_TYPES.SLEEP : EVENT_TYPES.NEED_ADD);
}
}, [copiedAppointment]);
useEffect(() => {
eventHandler.setEventType(copiedAppointment ? EVENT_TYPES.SLEEP : EVENT_TYPES.NEED_ADD);
bottomToolbar.set(
<AppointmentsToolbar onNext={next} onPrevious={previous} onHome={homeHandler} />
);
filter.setTitle("Filtrar Turnos");
filter.clearFilterData();
filter.setContent(
<FilterContent data={collaboratorFilterData} setCollaboratorId={setCollaboratorId} />
);
filter.setOnFilter(() => {});
}, [collaboratorFilterData]);
useEffect(() => {
const filter: FindEmployeesParams = {
companyId: id,
};
filterObjects<FindEmployeesParams>("employees/filter-all", filter).then((res) => {
setCollaboratorFilterData(res);
});
setDialogCalendarEvent(undefined);
}, []);
/* useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
console.log("currentDate:", currentDate);
}, [currentDate]); */
useEffect(() => {
if (windowWidth < 600) {
setViewMode(CalendarViewMode.ROW);
} else {
setViewMode(CalendarViewMode.COLUMN);
}
}, [windowWidth]);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Mis organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Organizacion",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Turnos",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
loadCalendarData();
setDialogCalendarEvent(undefined);
}, [currentDate, collaboratorId]);
useEffect(() => {
return () => {
bottomToolbar.clear();
};
}, []);
const loadCalendarData = () => {
const dataFind: FindAppointmentsParams = {
companyId: id,
dateDay: dayjs(currentDate).clone().startOf("day").toISOString(),
employeeId: appointmentsView.collaboratorId
? appointmentsView.collaboratorId
: collaboratorId,
};
const filteredFilter = Object.fromEntries(
Object.entries(dataFind).filter(([, v]) => v && v.toString().trim() !== "")
);
eventHandler.setEventType(EVENT_TYPES.LOADING);
findAppointmentsAdminByDate(filteredFilter)
.then((data) => {
setCalendarEventsCount(data.events.length);
setCalendarFrom(data.from);
setCalendarTo(data.to);
setCalendarData([
{
name: "",
events: data.events,
},
]);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(copiedAppointment ? EVENT_TYPES.SLEEP : EVENT_TYPES.NEED_ADD);
});
};
const next = (mode: ManipulateType = "day") => {
setCurrentDate((prevDate) => prevDate.clone().add(1, mode));
};
const previous = (mode: ManipulateType = "day") => {
setCurrentDate((prevDate) => prevDate.clone().subtract(1, mode));
};
const homeHandler = () => {
setCurrentDate(dayjs(new Date()).startOf("day"));
};
const handleAdd = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
goTo(
"/admin/org/profile/" +
id +
"/appointments/" +
encodeURIComponent(dayjs(currentDate).clone().startOf("day").toISOString()) +
"/create/"
);
};
const crearTurnoConRepeticion = () => {
if (!dialogCalendarEvent) {
return;
}
const serviceId = dialogCalendarEvent.serviceId;
const employeeId = dialogCalendarEvent.employeeId;
const repeatId = dialogCalendarEvent.repeatId;
const startHour = Math.floor(dialogCalendarEvent.startTime);
const startMinutes = (dialogCalendarEvent.startTime - startHour) * 60;
const startDate = dayjs(currentDate.clone().startOf("day"))
.hour(startHour)
.minute(startMinutes);
if (!serviceId || !employeeId) {
return;
}
const data: CreateAppointmentParams = {
companyId: id,
serviceId: serviceId,
employeeId: employeeId,
clientId: dialogCalendarEvent.clientId,
repeatId: repeatId,
start: startDate.toISOString(),
notification: false,
validation: false,
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
createAppointment(data)
.then(() => {
setDialogVisible(false);
loadCalendarData();
alert.showSuccess("Turno creado con éxito");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const handleMoveAppointment = async (newDay: dayjs.Dayjs) => {
if (!copiedAppointment || !copiedAppointment.appointmentId) return;
timePicker.hide();
eventHandler.setEventType(EVENT_TYPES.LOADING);
const newStart = dayjs(currentDate).hour(newDay.hour()).minute(newDay.minute()).second(0).millisecond(0).toISOString();
const oldStart = dayjs(copiedAppointment.startTime);
const oldEnd = dayjs(copiedAppointment.endTime);
const durationMin = oldEnd.diff(oldStart, 'minute');
const newEnd = dayjs(newStart).add(durationMin, 'minute').toISOString();
try {
await ApiRequest.post("/appointments/move-appointment-force", {
id: copiedAppointment.appointmentId,
companyId: id,
newStart: newStart,
newEnd: newEnd,
sessionUser: SessionInfo.userId,
});
alert.showSuccess("Turno movido con éxito");
setCopiedAppointment(null);
loadCalendarData();
} catch (error: unknown) {
console.error(error);
alert.showError("Error al mover el turno");
} finally {
eventHandler.setEventType(copiedAppointment ? EVENT_TYPES.SLEEP : EVENT_TYPES.NEED_ADD);
}
};
const anularTurnoConRepeticion = () => {
if (!dialogCalendarEvent) {
return;
}
if (!dialogCalendarEvent.repeatId) {
return;
}
if (!SessionInfo.userId) {
return;
}
const data: AddRepeatDisabledDateParams = {
repeatId: dialogCalendarEvent.repeatId,
disableDate: dayjs(
currentDate.clone().startOf("day").hour(dialogCalendarEvent.startTime)
).toDate(),
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
addDisabledRepeatDate(data)
.then(() => {
setDialogVisible(false);
loadCalendarData();
alert.showSuccess("Reserva anulada con éxito");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
return (
<>
<Addable onClick={handleAdd}>
<Filterable />
<Location />
<div className={style.appointmentsTitle}>
<h4>{dayjs(currentDate).format("dddd DD [de] MMMM, YYYY")}</h4>
</div>
<div className={style.appointmentsContent}>
{calendarEventsCount === 0 && (
<div>
<MaterialAlert>
<b>No hay turnos cargados para la fecha seleccionada.</b> Para
cargar un nuevo turno haga click en el boton inferior.
</MaterialAlert>
</div>
)}
{calendarEventsCount > 0 && viewMode === CalendarViewMode.ROW && (
<CalendarRowView
daySchedules={calendarData}
onEventLongPress={(event) => {
setCopiedAppointment(event); alert.showSuccess("ID del turno copiado exitosamente.");
}}
handleEventClick={(event) => {
if (!event.appointmentId) {
setDialogCalendarEvent(event);
setDialogVisible(true);
return;
}
setCopiedAppointment(null);
goTo(
"/admin/org/" +
id +
"/client/" +
event.clientId +
"/appointments/" +
event.appointmentId
);
}}
/>
)}
{calendarEventsCount > 0 && viewMode === CalendarViewMode.COLUMN && (
<div
>
<ScheduleView
daySchedules={calendarData}
viewStartTime={calendarFrom}
viewEndTime={calendarTo}
theme={scViewTurnosExpress(60)}
handleEventClick={(event) => {
if (!event.appointmentId) {
setDialogCalendarEvent(event);
setDialogVisible(true);
return;
}
setCopiedAppointment(null);
goTo(
"/admin/org/" +
id +
"/client/" +
event.clientId +
"/appointments/" +
event.appointmentId
);
}}
/>
</div>
)}
</div>
<ActionDialog
title="Crear turno"
message="Este turno aún no ha sido creado ya que pertenece a una reserva con repetición automática."
visible={dialogVisible}
setVisible={setDialogVisible}
buttons={[
<Button
key="cancelButton"
variant="contained"
color="secondary"
onClick={() => {
setDialogVisible(false);
}}
>
Cancelar
</Button>,
<Button
key="disableButton"
variant="contained"
color="warning"
onClick={() => {
anularTurnoConRepeticion();
}}
>
Anular
</Button>,
<Button
key="createButton"
variant="contained"
color="success"
onClick={() => {
crearTurnoConRepeticion();
}}
>
Crear
</Button>,
]}
/>
{copiedAppointment && (
<>
<div
className={style.buttonCancelPaste}
onClick={() => {
setCopiedAppointment(null);
}}
>
<CloseIcon />
</div>
<div
className={style.buttonPaste}
onClick={() => timePicker.show((newDay) => handleMoveAppointment(newDay))}
>
<ContentPasteIcon />
</div>
</>
)}
</Addable>
</>
);
}
@@ -0,0 +1,35 @@
import ApiRequest from "@services/Api.Service";
import * as Yup from "yup";
import { ApiError } from "@core/Models/Server.Error.model";
import { FindAssistanceUserParams, UserAssistanceInfo } from "@core/Models/User.model";
const schemaGetUserAssistance = Yup.object().shape({
email: Yup.string()
.email("Debe ingresar un e-mail válido.")
.required("Debe introducir el e-mail."),
sessionUser: Yup.string().required("Debe iniciar sessión."),
});
export const getAssistanceUserInfo = async (
data: FindAssistanceUserParams
): Promise<UserAssistanceInfo> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaGetUserAssistance.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new ApiError(400, yupErrors.message);
}
return ApiRequest.post<UserAssistanceInfo>("users/assistance-info", data);
};
@@ -0,0 +1,233 @@
"use client";
import { useLocationStore } from "@store/Location.Store";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import SearchText from "@components/SearchText/SearchText";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useAlert } from "@store/Alert.Store";
import MaterialAlert from "@mui/material/Alert";
import { UserAssistanceInfo } from "@core/Models/User.model";
import Link from "next/link";
import { getAssistanceUserInfo } from "./Assistance.Service";
import AssistanceIcon from "@mui/icons-material/HeadsetMicOutlined";
type UrlData = {
id: string;
};
export default function CollaboratorsAdd() {
const urlData = useParams<UrlData>();
const id = urlData.id;
const alert = useAlert();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const SessionInfo = useSessionStore();
const [user, setUser] = useState<UserAssistanceInfo | null>(null);
const [findEmail, setFindEmail] = useState<string>("");
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id,
title: "Organizacion",
icon: null,
});
location.add({
url: "",
title: "Asistencia",
icon: <AssistanceIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
const findUser = async () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
getAssistanceUserInfo({ email: findEmail, sessionUser: SessionInfo.userId })
.then((response) => {
setUser(response);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
return (
<>
<Location />
<div
style={{ display: "flex", flexDirection: "column", marginTop: "20px", gap: "10px" }}
>
<SearchText
value={findEmail}
width="100%"
placeHolder="Buscar por e-mail"
actionText="Buscar"
onChange={(e) => {
setFindEmail(e.target.value.toLowerCase().trim());
}}
onClick={findUser}
/>
{!user && (
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Atención:</b> El usuario debe estar registrado en TurnosXpress con el
correo electrónico ingresado.
</MaterialAlert>
)}
{user && !user.email && (
<>
<MaterialAlert severity="error" sx={{ marginTop: "20px" }}>
<b>Atención:</b> No se ha encontrado un usuario con el e-mail ingresado.
</MaterialAlert>
</>
)}
{user && user.email && user.verificated && !user.recoveryCode && (
<>
<MaterialAlert severity="success" sx={{ marginTop: "20px" }}>
<b>Atención:</b> El usuario asociado al correo electrónico ingresado ya
se encuentra activo.
</MaterialAlert>
</>
)}
{user && user.email && user.verificated && user.recoveryCode && (
<>
<MaterialAlert severity="success" sx={{ marginTop: "20px" }}>
<b>Atención:</b> {user.message}
</MaterialAlert>
</>
)}
{user && user.email && !user.verificated && (
<>
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: "10px",
backgroundColor: "var(--white-dark)",
border: "1px solid var(--gray-light)",
borderRadius: "10px",
padding: "20px",
maxWidth: "600px",
marginLeft: "auto",
marginRight: "auto",
}}
>
<h1
style={{
color: "var(--wine-dark)",
paddingBottom: "0px",
marginBottom: "0px",
marginTop: "0px",
paddingTop: "0px",
}}
>
{user.fullName}
</h1>
<h2 style={{ color: "var(--black)", fontSize: "16px" }}>
{user.email}
</h2>
<p style={{ color: "var(--black)", marginTop: "15px" }}>
El usuario esta inactivo. Para activarlo debe ingresar en{" "}
<Link href={"/landing/login/verification"}>
https://turnosxpress.com.ar/landing/login/verification
</Link>{" "}
con el siguiente código de activación:
</p>
<div
style={{
marginLeft: "auto",
marginRight: "auto",
marginTop: "10px",
marginBottom: "10px",
padding: "10px 20px",
border: "1px solid var(--green-darkest)",
borderRadius: "10px",
backgroundColor: "var(--green-light)",
fontSize: "36px",
fontWeight: "bold",
color: "var(--black)",
letterSpacing: "10px",
}}
>
{user.activationCode}
</div>
</div>
</>
)}
{user && user.email && user.verificated && user.recoveryCode && (
<>
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: "10px",
backgroundColor: "var(--white-dark)",
border: "1px solid var(--gray-light)",
borderRadius: "10px",
padding: "20px",
maxWidth: "600px",
marginLeft: "auto",
marginRight: "auto",
}}
>
<h1
style={{
color: "var(--wine-dark)",
paddingBottom: "0px",
marginBottom: "0px",
marginTop: "0px",
paddingTop: "0px",
}}
>
{user.fullName}
</h1>
<h2 style={{ color: "var(--black)", fontSize: "16px" }}>
{user.email}
</h2>
<p style={{ color: "var(--black)", marginTop: "15px" }}>
El usuario esta activo. Para cambiar la clave debe ingresar en{" "}
<Link href={"/landing/recover-account/verify"}>
https://turnosxpress.com.ar/landing/recover-account/verify
</Link>{" "}
con el siguiente código de recuperación:
</p>
<div
style={{
marginLeft: "auto",
marginRight: "auto",
marginTop: "10px",
marginBottom: "10px",
padding: "10px 20px",
border: "1px solid var(--green-darkest)",
borderRadius: "10px",
backgroundColor: "var(--green-light)",
fontSize: "36px",
fontWeight: "bold",
color: "var(--black)",
letterSpacing: "10px",
}}
>
{user.recoveryCode}
</div>
</div>
</>
)}
</div>
</>
);
}
@@ -0,0 +1,74 @@
import { ApiVoidResult } from "@core/Models/Api.VoidResult.type";
import ApiRequest from "@services/Api.Service";
import {
CalculateCashFlowBalanceParams,
CashFlowHistoryView,
CreateCashFlowParams,
DeleteMovementParams,
FindCashFlowBalanceParams,
ICashFlowBalance,
PaginateCashFlowParams,
PaginateCashFlowResults,
} from "@core/Models/CashFlow.model";
import { ApiError } from "@core/Models/Server.Error.model";
import * as Yup from "yup";
const schemaCreateCashFlowMovement = Yup.object().shape({
paymentMethod: Yup.string().required("Debe introducir el tipo de movimiento."),
companyId: Yup.string().required("La organización no es valida."),
amount: Yup.number()
.positive("El monto debe ser un número positivo.")
.required("Debe introducir el monto del movimiento.")
.typeError("El monto debe ser un número."),
description: Yup.string().required("Debe introducir una descripción."),
sessionUser: Yup.string().required("Para crear una organización debe iniciar sesión."),
});
export const createCashFlow = async (data: CreateCashFlowParams): Promise<ApiVoidResult> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaCreateCashFlowMovement.validate(data, {
abortEarly: true,
});
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new ApiError(400, yupErrors.message);
}
return ApiRequest.post<ApiVoidResult>("cashflow/create", data);
};
export const cashFlowCalculateBalance = async (
data: CalculateCashFlowBalanceParams
): Promise<ICashFlowBalance> => {
return ApiRequest.post<ICashFlowBalance>("cashflow/calculate-balance", data);
};
export const cashFlowDelete = async (data: DeleteMovementParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("cashflow/delete", data);
};
export const cashFlowHistory = async (
data: FindCashFlowBalanceParams
): Promise<CashFlowHistoryView> => {
return ApiRequest.post<CashFlowHistoryView>("cashflow/history", data);
};
export const cashFlowPaginate = async (
data: PaginateCashFlowParams
): Promise<PaginateCashFlowResults> => {
return ApiRequest.post<PaginateCashFlowResults>("cashflow/paginate", data);
};
export const cashFlowPaginateBalance = async (data: FindCashFlowBalanceParams): Promise<number> => {
return ApiRequest.post<number>("cashflow/paginate-balance", data);
};
@@ -0,0 +1,238 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import AccountBalanceOutlinedIcon from "@mui/icons-material/AccountBalanceOutlined";
import Textbox from "@components/Textbox/Textbox";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import AccountMovementsIcon from "@mui/icons-material/CurrencyExchangeOutlined";
import {
CLIENT_ACCOUNT_MOVEMENT_TYPES,
CLIENT_ACCOUNT_PAYMENT_METHODS,
} from "@core/Models/ClientAccountMovements.model";
import Button from "@core/app/components/Button/Button";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import { CreateCashFlowParams } from "@core/Models/CashFlow.model";
import dayjs from "dayjs";
import { createCashFlow } from "../../Cash.Service";
import { PAYMENT_PROCCESS_DATE } from "@core/Models/Payments.model";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
type UrlData = {
id: string;
type: CLIENT_ACCOUNT_MOVEMENT_TYPES;
};
export default function CashFlowMovementAdd() {
const data = useParams<UrlData>();
const id = data.id;
const type = data.type;
const alert = useAlert();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const [amount, setAmount] = useState<string>("");
const [description, setDescription] = useState<string>("");
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/",
title: "Organización",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/cash",
title: "Caja",
icon: <AccountBalanceOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: getActionName(),
icon: <AccountMovementsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
const getActionName = () => {
if (type === CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT) {
return "Depositar";
}
return "Retirar";
};
const [paymentMethodFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Método de pago:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el nombre del tipo de movieminto para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [paymentMethhodFilterData, setPaymentMethodFilterData] = useState<
TextObjectFilterResult[]
>([]);
const [paymentMethodId, setPaymentMethodId] = useState<string>("");
useEffect(() => {
loadPaymentMethods();
}, []);
const loadPaymentMethods = () => {
const methods = [
{
id: CLIENT_ACCOUNT_PAYMENT_METHODS.CASH,
name: "Efectivo",
description: "Efectivo",
},
{
id: CLIENT_ACCOUNT_PAYMENT_METHODS.BANK_TRANSFER,
name: "Transferencia",
description: "Transferencia",
},
{
id: CLIENT_ACCOUNT_PAYMENT_METHODS.CREDIT_CARD,
name: "Tarjeta de crédito",
description: "Tarjeta de crédito",
},
{
id: CLIENT_ACCOUNT_PAYMENT_METHODS.DEBIT_CARD,
name: "Tarjeta de débito",
description: "Tarjeta de débito",
},
{
id: CLIENT_ACCOUNT_PAYMENT_METHODS.OTHER,
name: "Otro",
description: "Otro",
},
];
setPaymentMethodFilterData(
methods.map((category) => ({
id: category.id.toString(),
name: category.name,
description: category.description,
}))
);
};
const addMovement = () => {
const currentDate = dayjs();
const currentYeath = currentDate.year();
const currentMonth = currentDate.month();
const data: CreateCashFlowParams = {
companyId: id,
amount: Number(amount),
month: currentMonth,
year: currentYeath,
paymentMethod: paymentMethodId as CLIENT_ACCOUNT_PAYMENT_METHODS,
description: description,
reference: "",
type: type,
proccessDate: PAYMENT_PROCCESS_DATE.NOW,
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
createCashFlow(data)
.then(() => {
alert.showSuccess("Movimiento agregado exitosamente");
goTo("/admin/org/profile/" + id + "/cash");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
});
};
return (
<>
<Location />
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<>
<div style={{ marginTop: "15px" }}></div>
<TextObjectFilter
placeholder="Tipo de movimiento"
content={paymentMethodFilterContent}
data={paymentMethhodFilterData}
onChange={(id) => {
setPaymentMethodId(id);
}}
/>
<Textbox
placeholder="Monto"
value={amount}
type="number"
width="100%"
onChange={(e) => setAmount(e.target.value)}
/>
<Textbox
placeholder="Descripción"
value={description}
type="comments"
width="100%"
style={{ height: "100px" }}
limitSize={100}
onChange={(e) => setDescription(e.target.value)}
/>
<Button
text={getActionName()}
onClick={() => {
addMovement();
}}
color="primary"
width="100%"
style={{ marginTop: "15px" }}
/>
<div style={{ marginTop: "15px" }}></div>
</>
</SecureByPlan>
</>
);
}
@@ -0,0 +1,381 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import AccountBalanceOutlinedIcon from "@mui/icons-material/AccountBalanceOutlined";
import AccountMovementsIcon from "@mui/icons-material/CurrencyExchangeOutlined";
import DataGrid, { DataGridColumn } from "@core/app/components/DataGrid/DataGrid";
import { formatPrice } from "@core/app/helpers/Numbers";
import dayjs from "dayjs";
import {
ICashFlow,
PaginateCashFlowParams,
PaginateCashFlowResults,
} from "@core/Models/CashFlow.model";
import { cashFlowPaginate } from "../Cash.Service";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
import Filterable from "@core/app/components/Filterable/Filterable";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import { useFilterStore } from "@core/Store/Filter.Store";
import { FindEmployeesParams } from "@core/Models/Collaborators.model";
import filterObjects from "@components/TextObjectFilter/Filter.Service";
import { FindClientsParams } from "@core/Models/Clients.model";
import { paymentMethods, PAYMENT_METHODS } from "@core/Models/Payments.model";
type UrlData = {
id: string;
};
interface FilterContentProps {
collaboratorsData: TextObjectFilterResult[];
setCollaboratorId: (id: string) => void;
methodsData: TextObjectFilterResult[];
setMethodId: (id: string) => void;
clientData: TextObjectFilterResult[];
setClientId: (id: string) => void;
}
const FilterContent = (props: FilterContentProps) => {
const [clientFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Cliente:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el nombre del cliente para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [collaboratorFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Colaborador:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el nombre del colaborador para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [methodFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Método de Pago:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el nombre del método de pago para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
return (
<>
<TextObjectFilter
placeholder="Cliente"
content={clientFilterContent}
data={props.clientData}
onChange={(id) => {
props.setClientId(id);
}}
/>
<TextObjectFilter
placeholder="Colaborador"
content={collaboratorFilterContent}
data={props.collaboratorsData}
onChange={(id) => {
props.setCollaboratorId(id);
}}
/>
<TextObjectFilter
placeholder="Método de Pago"
content={methodFilterContent}
data={props.methodsData}
onChange={(id) => {
props.setMethodId(id);
}}
/>
</>
);
};
export default function CashFlowMovementsList() {
const data = useParams<UrlData>();
const id = data.id;
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const filter = useFilterStore();
const [clientFilterData, setClientFilterData] = useState<TextObjectFilterResult[]>([]);
const [clientId, setClientId] = useState<string>("");
const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>(
[]
);
const [collaboratorId, setCollaboratorId] = useState<string>("");
const [methodFilterData, setMethodFilterData] = useState<TextObjectFilterResult[]>([]);
const [methodId, setMethodId] = useState<string>("");
const [dataGridColumns] = useState<DataGridColumn<ICashFlow>[]>([
{
field: "description",
title: "Detalle",
width: null,
render: (value: ICashFlow) => (
<>
<div
style={{
paddingLeft: "10px",
paddingRight: "10px",
paddingTop: "10px",
}}
>
{value.description}
</div>
<div
style={{
paddingLeft: "10px",
paddingRight: "10px",
color: "var(--gray-dark)",
}}
>
{dayjs(value.createdAt).format("DD MMMM YYYY - HH:mm")}
</div>
</>
),
},
{
field: "amount",
title: "Importe",
width: 150,
render: (value: ICashFlow) => (
<div
style={{
padding: "10px",
textAlign: "right",
color: value.amount > 0 ? "var(--green-darkestX2)" : "var(--red)",
}}
>
{formatPrice(value.amount)}
</div>
),
},
]);
const [pageSize] = useState<number>(
parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
: "10"
)
);
const [movements, setMovements] = useState<PaginateCashFlowResults>();
const movementsRef = useRef(movements);
const loadMovements = (page: number = 1, pageSize: number = 30) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const filterData: PaginateCashFlowParams = {
companyId: id,
page: page,
limit: pageSize,
sessionUser: SessionInfo.userId,
};
if (clientId) {
filterData.clientId = clientId;
}
if (collaboratorId) {
filterData.employeeId = collaboratorId;
}
if (methodId) {
filterData.paymentMethod = methodId as PAYMENT_METHODS;
}
cashFlowPaginate(filterData)
.then((res) => {
setMovements(res);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
});
};
useEffect(() => {
const filter: FindClientsParams = {
companyId: id,
};
filterObjects<FindClientsParams>("clients/filter-all", filter).then((res) => {
setClientFilterData(res);
});
}, []);
useEffect(() => {
const filter: FindEmployeesParams = {
companyId: id,
};
filterObjects<FindEmployeesParams>("employees/filter-all", filter).then((res) => {
setCollaboratorFilterData(res);
});
}, []);
useEffect(() => {
setMethodFilterData(paymentMethods);
}, []);
useEffect(() => {
filter.setTitle("Filtrar Movimientos");
filter.clearFilterData();
filter.setContent(
<FilterContent
collaboratorsData={collaboratorFilterData}
setCollaboratorId={setCollaboratorId}
methodsData={methodFilterData}
setMethodId={setMethodId}
clientData={clientFilterData}
setClientId={setClientId}
/>
);
filter.setOnFilter(() => { });
}, [collaboratorFilterData, methodFilterData, clientFilterData]);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/cash",
title: "Caja",
icon: <AccountBalanceOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Movimientos",
icon: <AccountMovementsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
movementsRef.current = movements;
}, [movements]);
useEffect(() => {
let loadPage = 1;
if (movementsRef.current) {
loadPage = movementsRef.current.page;
}
loadMovements(loadPage, pageSize);
}, [clientId, collaboratorId, methodId]);
return (
<>
<Location />
<Filterable />
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<>
<div
style={{
position: "absolute",
top: "80px",
left: "0px",
right: "0px",
bottom: "30px",
}}
>
<DataGrid
columns={dataGridColumns}
data={movements ? movements.data : []}
pagination={true}
page={movements?.page}
pages={movements?.pages}
pageSize={pageSize}
handleRefresh={loadMovements}
handleRowClick={() => { }}
/>
</div>
<div style={{ marginTop: "15px" }}></div>
</>
</SecureByPlan>
</>
);
}
@@ -0,0 +1,349 @@
"use client";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import PaymentsIcon from "@mui/icons-material/CurrencyExchangeOutlined";
import { ThemeProvider } from "@emotion/react";
import turnosXpressTheme from "@core/app/theme/turnosXpress";
import { ButtonGroup } from "@mui/material";
import ButtonMaterial from "@mui/material/Button";
import { useNavigation } from "@core/app/hooks/goto";
import { PaginateCashFlowParams, PaginateCashFlowResults } from "@core/Models/CashFlow.model";
import { useSessionStore } from "@core/Store/Sesion.Store";
import { cashFlowPaginate } from "./Cash.Service";
import { useAlert } from "@core/Store/Alert.Store";
import { IconButton } from "@mui/material";
import RetirarIcon from "@mui/icons-material/CallMadeOutlined";
import DepositarIcon from "@mui/icons-material/SouthWestOutlined";
import { formatPrice } from "@core/app/helpers/Numbers";
import MovimientosIcon from "@mui/icons-material/Reorder";
import TextRangeDate from "@core/app/components/TextRangeDate/TextRangeDate";
import dayjs, { Dayjs } from "dayjs";
import { getPaymentIcon } from "@core/app/components/PaymentIcons/PaymentIcons";
import { CLIENT_ACCOUNT_MOVEMENT_TYPES } from "@core/Models/ClientAccountMovements.model";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
type UrlData = {
id: string;
};
export default function OrganizationCashFlowDetail() {
const data = useParams<UrlData>();
const id = data.id;
const alert = useAlert();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const { goTo } = useNavigation();
const [dateFrom, setDateFrom] = useState<Dayjs>(dayjs().startOf("day"));
const [dateTo, setDateTo] = useState<Dayjs>(dayjs().endOf("day"));
const [view, setView] = useState<PaginateCashFlowResults | null>(null);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Perfil",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Caja",
icon: <PaymentsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
cargarDatos();
}, [SessionInfo.userId, id, dateFrom, dateTo]);
const cargarDatos = async () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
if (!SessionInfo.userId) {
return;
}
if (!id) {
return;
}
const filterData: PaginateCashFlowParams = {
companyId: id,
sessionUser: SessionInfo.userId,
page: 1,
limit: 10,
};
if (dateFrom) {
filterData.startDate = dateFrom.toDate();
}
if (dateTo) {
filterData.endDate = dateTo.toDate();
}
cashFlowPaginate(filterData)
.then((data) => {
setView(data);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
});
};
const add = (type: CLIENT_ACCOUNT_MOVEMENT_TYPES) => {
goTo("/admin/org/profile/" + id + "/cash/add/" + type);
};
return (
<>
<Location />
<div style={{ display: "flex", justifyContent: "center", marginTop: "15px" }}>
<ThemeProvider theme={turnosXpressTheme}>
<ButtonGroup variant="outlined" aria-label="Basic button group">
<ButtonMaterial variant="contained">Detalle</ButtonMaterial>
<ButtonMaterial
variant="outlined"
onClick={() => {
goTo(`/admin/org/profile/${id}/cash/resume`);
}}
>
Resumen
</ButtonMaterial>
</ButtonGroup>
</ThemeProvider>
</div>
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<>
{view && (
<>
<TextRangeDate
label="Fecha"
dateFrom={dateFrom}
dateTo={dateTo}
setDateFrom={setDateFrom}
setDateTo={setDateTo}
style={{ marginTop: "20px" }}
/>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
backgroundColor: "var(--white-dark)",
borderRadius: "10px",
padding: "10px",
marginTop: "15px",
}}
>
<div
style={{
fontSize: "16px",
fontWeight: "bold",
color: "var(--wine-red)",
}}
>
SALDO
</div>
<div
style={{
fontSize: "45px",
fontWeight: "bold",
color: "var(--wine-red)",
}}
>
{formatPrice(view.balance)}
</div>
<div
style={{
display: "flex",
gap: "10px",
flexDirection: "row",
}}
>
<IconButton
sx={{
color: "var(--green-darkestX2)",
display: "flex",
alignItems: "center",
gap: "10px",
flexDirection: "column",
width: "100px",
}}
onClick={() => add(CLIENT_ACCOUNT_MOVEMENT_TYPES.DEPOSIT)}
>
<DepositarIcon sx={{ fontSize: "48px" }} />
<span style={{ fontSize: "14px" }}>Depositar</span>
</IconButton>
<IconButton
sx={{
color: "var(--red-darkest)",
display: "flex",
alignItems: "center",
gap: "10px",
flexDirection: "column",
width: "100px",
}}
onClick={() =>
add(CLIENT_ACCOUNT_MOVEMENT_TYPES.WITHDRAWAL)
}
>
<RetirarIcon sx={{ fontSize: "48px" }} />
<span style={{ fontSize: "14px" }}>Retirar</span>
</IconButton>
<IconButton
sx={{
color: "var(--wine-dark)",
display: "flex",
alignItems: "center",
gap: "10px",
flexDirection: "column",
width: "100px",
}}
onClick={() => {
goTo("/admin/org/profile/" + id + "/cash/movements");
}}
>
<MovimientosIcon sx={{ fontSize: "48px" }} />
<span style={{ fontSize: "14px" }}>Movimientos</span>
</IconButton>
</div>
</div>
</>
)}
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
marginTop: "15px",
backgroundColor: "var(--white-dark)",
borderRadius: "10px",
padding: "20px",
}}
>
<div
style={{
fontSize: "16px",
fontWeight: "bold",
marginBottom: "10px",
}}
>
Últimos Movimientos
</div>
{view && view.data.length > 0 ? (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
}}
>
{view.data.map((movement, index) => (
<div
key={"movement" + index}
style={{
width: "100%",
}}
>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "flex-start",
justifyContent: "space-between",
width: "100%",
marginBottom: "10px",
borderBottom:
index < view.data.length - 1
? "1px solid var(--white-darkest)"
: "none",
}}
>
<div
style={{
maxWidth: "50px",
minWidth: "50px",
display: "flex",
alignItems: "center",
justifyContent: "center",
marginTop: "5px",
}}
>
{getPaymentIcon(movement.paymentMethod)}
</div>
<div style={{ width: "100%" }}>
<div style={{ fontSize: "16px" }}>
{movement.description}
</div>
<div
style={{
fontSize: "14px",
color: "var(--gray-dark)",
marginBottom: "10px",
}}
>
{dayjs(movement.createdAt).format(
"DD MMMM YYYY - HH:mm"
)}
</div>
</div>
<div
style={{
fontSize: "16px",
fontWeight: "bold",
color:
movement.amount > 0
? "var(--green-darkestX2)"
: "var(--red-darkest)",
backgroundColor:
movement.amount > 0
? "var(--green-light)"
: "var(--red-light)",
padding: "5px 10px",
borderRadius: "5px",
}}
>
{formatPrice(Math.abs(movement.amount))}
</div>
</div>
</div>
))}
</div>
) : (
<div style={{ fontSize: "16px", fontWeight: "bold" }}>
No hay movimientos
</div>
)}
</div>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
</SecureByPlan>
</>
);
}
@@ -0,0 +1,333 @@
"use client";
import { useParams } from "next/navigation";
import { useEffect, useState } from "react";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import PaymentsIcon from "@mui/icons-material/CurrencyExchangeOutlined";
import { ThemeProvider } from "@emotion/react";
import turnosXpressTheme from "@core/app/theme/turnosXpress";
import { ButtonGroup, DialogActions, DialogContent } from "@mui/material";
import ButtonMaterial from "@mui/material/Button";
import { useNavigation } from "@core/app/hooks/goto";
import { CashFlowHistoryView, FindCashFlowBalanceParams } from "@core/Models/CashFlow.model";
import { useSessionStore } from "@core/Store/Sesion.Store";
import { useAlert } from "@core/Store/Alert.Store";
import dayjs from "dayjs";
import { cashFlowCalculateBalance, cashFlowHistory } from "../Cash.Service";
import YearInput from "@core/app/components/YearInput/YearInput";
import { formatPrice } from "@core/app/helpers/Numbers";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
import CalculateIcon from "@mui/icons-material/EventRepeatOutlined";
import DialogWindow from "@core/app/components/DialogWindow/DialogWindow";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
import TextMonth from "@core/app/components/TextMonth/TextMonth";
type UrlData = {
id: string;
};
export default function OrganizationCashFlowResume() {
const data = useParams<UrlData>();
const id = data.id;
const alert = useAlert();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const { goTo } = useNavigation();
const bottomToolbar = useBottomToolbarStore();
const [dialogCalcVisible, setDialogCalcVisible] = useState<boolean>(false);
const [year, setYear] = useState<number>(dayjs().year());
const [month, setMonth] = useState<number>(1);
const [view, setView] = useState<CashFlowHistoryView | null>(null);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Perfil",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Caja",
icon: <PaymentsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
bottomToolbar.set(
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
width: "100%",
flexDirection: "row",
}}
>
<ButtonMaterial
variant="text"
startIcon={<CalculateIcon />}
sx={{ color: "var(--white)" }}
onClick={() => {
setDialogCalcVisible(true);
}}
>
Recalcular Balance
</ButtonMaterial>
</div>
);
return () => {
bottomToolbar.clear();
};
}, []);
useEffect(() => {
cargarDatos();
}, [SessionInfo.userId, id, year]);
const cargarDatos = async () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
if (!SessionInfo.userId) {
return;
}
if (!id) {
return;
}
const filterData: FindCashFlowBalanceParams = {
companyId: id,
sessionUser: SessionInfo.userId,
year: year,
month: 6,
};
cashFlowHistory(filterData)
.then((data) => {
setView(data);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
});
};
const calculateBalanceHandler = () => {
if (!SessionInfo.userId) {
return;
}
if (!id) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
cashFlowCalculateBalance({
companyId: id,
year: year,
month: month,
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("Balance calculado correctamente");
setDialogCalcVisible(false);
setMonth(1);
cargarDatos();
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
});
};
return (
<>
<Location />
<div style={{ display: "flex", justifyContent: "center", marginTop: "15px" }}>
<ThemeProvider theme={turnosXpressTheme}>
<ButtonGroup variant="outlined" aria-label="Basic button group">
<ButtonMaterial
variant="outlined"
onClick={() => {
goTo(`/admin/org/profile/${id}/cash`);
}}
>
Detalle
</ButtonMaterial>
<ButtonMaterial variant="contained">Resumen</ButtonMaterial>
</ButtonGroup>
</ThemeProvider>
</div>
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<>
<YearInput
year={year}
setYear={setYear}
style={{ marginTop: "15px", height: "50px" }}
/>
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
marginTop: "15px",
backgroundColor: "var(--white-dark)",
borderRadius: "10px",
padding: "20px",
}}
>
<div
style={{
fontSize: "16px",
fontWeight: "bold",
marginBottom: "10px",
}}
>
Resumen
</div>
{view && view.data.length > 0 ? (
<div
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "100%",
}}
>
{view.data.map((monthData, index) => (
<div
key={"monthData" + index}
style={{
width: "100%",
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
}}
>
<div
style={{
fontSize: "16px",
textTransform: "capitalize",
}}
>
{dayjs().month(monthData.month).format("MMMM")}
</div>
<div
style={{
fontSize: "16px",
}}
>
{formatPrice(monthData.amount)}
</div>
</div>
))}
<div
style={{
width: "100%",
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
marginTop: "10px",
}}
>
<div
style={{
fontSize: "16px",
fontWeight: "bold",
textTransform: "capitalize",
}}
>
Total
</div>
<div
style={{
fontSize: "16px",
fontWeight: "bold",
}}
>
{formatPrice(view.total)}
</div>
</div>
</div>
) : (
<div style={{ fontSize: "16px", fontWeight: "bold" }}>
No hay movimientos
</div>
)}
</div>
<DialogWindow visible={dialogCalcVisible} setVisible={setDialogCalcVisible}>
<>
<DialogContent
style={{
maxWidth: "500px",
minWidth: "300px",
margin: "auto",
}}
>
<TextMonth
label="Seleccionar Mes"
month={month}
setMonth={setMonth}
style={{ marginTop: "15px", height: "50px", width: "300px" }}
/>
</DialogContent>
<DialogActions>
<ButtonMaterial
variant="contained"
color="primary"
onClick={calculateBalanceHandler}
>
Recalcular
</ButtonMaterial>
<ButtonMaterial
variant="contained"
color="secondary"
onClick={() => {
setDialogCalcVisible(false);
}}
>
Cancelar
</ButtonMaterial>
</DialogActions>
</>
</DialogWindow>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
</SecureByPlan>
</>
);
}
@@ -0,0 +1,50 @@
import ApiRequest from "@services/Api.Service";
import {
IClient,
OrganizationClientsResults,
CreateCustomClientParams,
PaginateOrganizationClientsParams,
} from "@models/Clients.model";
import * as Yup from "yup";
const schemaClientAdd = Yup.object().shape({
firstName: Yup.string().required("Debe introducir el nombre del cliente."),
lastName: Yup.string().required("Debe introducir el apellido del cliente."),
email: Yup.string().required().email("Debe introducir un E-mail valido."),
phoneAreaCode: Yup.number()
.required("Debe introducir el código de area.")
.typeError("El código de área debe ser un número."),
phoneNumber: Yup.number()
.required("Debe introducir el número de teléfono.")
.typeError("El número de teléfono debe ser un número."),
sessionUser: Yup.string().required("Para crear una organización debe iniciar sesión."),
});
export const createClient = async (data: CreateCustomClientParams): Promise<IClient> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaClientAdd.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new Error(yupErrors.message);
}
return ApiRequest.post<IClient>("clients/create", data);
};
export const findClientsByOrganization = async (
data: PaginateOrganizationClientsParams
): Promise<OrganizationClientsResults> => {
return ApiRequest.post<OrganizationClientsResults>("clients/find", data);
};
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function CreateClientLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,150 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import Textbox from "@components/Textbox/Textbox";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import TextPhone from "@core/app/components/TextPhone/TextPhone";
import MaterialAlert from "@mui/material/Alert";
import { createClient } from "../Clients.Service";
type UrlData = {
id: string;
};
export default function ClientCreate() {
const data = useParams<UrlData>();
const id = data.id;
const alert = useAlert();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [email, setEmail] = useState("");
const [phoneCountryCode] = useState("549");
const [phoneAreaCode, setPhoneAreaCode] = useState("");
const [phoneNumber, setPhoneNumber] = useState("");
const { goTo } = useNavigation();
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id,
title: "Origanizacion",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/clients",
title: "Clientes",
icon: <ClientsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/create",
title: "Crear",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
createClient({
companyId: id,
firstName: firstName,
lastName: lastName,
email: email,
phoneCountryCode: phoneCountryCode,
phoneAreaCode: phoneAreaCode,
phoneNumber: phoneNumber,
sessionUser: SessionInfo.userId,
})
.then((newClient) => {
alert.showSuccess(
`El cliente ${newClient.firstName} ${newClient.lastName} ha sido creado`
);
setTimeout(() => {
goTo("/admin/org/profile/" + id + "/clients");
}, 2000);
})
.catch((error) => {
alert.showError(error.message);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
return (
<>
<Location />
<MaterialAlert sx={{ width: "100%", marginTop: "10px" }} severity="info">
<strong>Nuevo cliente:</strong> Desde aquí puedes crear un cliente para tu
organización. Si el cliente se registra en TurnosXpress, se generará automáticamente
al hacer una reservación. Si no está registrado, puedes crear un cliente para hacer
reservas a su nombre, y luego fusionarlo con el nuevo registro para evitar
duplicados.
</MaterialAlert>
<Textbox
placeholder="Nombre"
type="text"
value={firstName}
width="100%"
onChange={(e) => {
setFirstName(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
placeholder="Apellido"
type="text"
value={lastName}
width="100%"
onChange={(e) => {
setLastName(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
placeholder="E-mail"
type="email"
value={email}
width="100%"
onChange={(e) => {
setEmail(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<TextPhone
label="Teléfono"
areaCode={phoneAreaCode}
number={phoneNumber}
width="100%"
onChangeArea={(e) => {
setPhoneAreaCode(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
onChangePhone={(e) => {
setPhoneNumber(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<div style={{ marginTop: "15px" }}></div>
</>
);
}
@@ -0,0 +1,28 @@
"use client";
import { useParams } from "next/navigation";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import { useNavigation } from "@hooks/goto";
import Addable from "@components/Addable/Addable";
type UrlData = {
id: string;
};
export default function ClientLayout({ children }: { children: React.ReactNode }) {
const urlData = useParams<UrlData>();
const id = urlData.id;
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const handleAdd = () => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
goTo("/admin/org/profile/" + id + "/clients/create");
};
return (
<>
<Addable onClick={handleAdd}>{children}</Addable>
</>
);
}
@@ -0,0 +1,296 @@
.pageContainer {
display: flex;
flex-direction: column;
position: absolute;
top: 60px; /* Below Location/Filterable */
left: 0;
right: 0;
bottom: 10px;
background: transparent;
border-radius: 24px;
overflow: hidden;
}
@media (max-width: 768px) {
.pageContainer {
border-radius: 16px;
}
}
.clientsGrid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
padding: 30px;
overflow-y: auto;
flex-grow: 1;
z-index: 1;
align-content: start;
}
@media (max-width: 768px) {
.clientsGrid {
grid-template-columns: 1fr;
padding: 16px;
gap: 16px;
}
}
.clientCard {
background: #ffffff;
border-radius: 20px;
padding: 24px;
display: flex;
align-items: center;
gap: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03), 0 1px 4px rgba(0, 0, 0, 0.02);
border: 1px solid rgba(226, 232, 240, 1);
cursor: pointer;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
}
@media (max-width: 768px) {
.clientCard {
padding: 16px;
gap: 16px;
align-items: flex-start; /* Prevents vertical squishing of content */
border-radius: 16px;
}
}
/* Accent line on hover */
.clientCard::after {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: var(--wine);
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 4px 0 0 4px;
}
@media (min-width: 769px) {
.clientCard:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.05), 0 3px 8px rgba(0, 0, 0, 0.02);
border-color: rgba(226, 232, 240, 1);
}
.clientCard:hover::after {
opacity: 1;
}
}
/* Banned Card Styles */
.clientCard.banned {
background: #fffafa;
border: 1px solid rgba(254, 226, 226, 1);
box-shadow: 0 4px 15px rgba(239, 68, 68, 0.03);
}
.clientCard.banned::after {
background: #ef4444;
opacity: 1;
}
@media (min-width: 769px) {
.clientCard.banned:hover {
box-shadow: 0 12px 30px rgba(239, 68, 68, 0.08);
}
}
/* Alternating Pastel Colors for Cards */
.clientCard:nth-child(6n + 1):not(.banned) {
background: #f0f9ff;
border-color: #e0f2fe;
}
.clientCard:nth-child(6n + 2):not(.banned) {
background: #f5f3ff;
border-color: #ede9fe;
}
.clientCard:nth-child(6n + 3):not(.banned) {
background: #f0fdf4;
border-color: #dcfce7;
}
.clientCard:nth-child(6n + 4):not(.banned) {
background: #fff7ed;
border-color: #ffedd5;
}
.clientCard:nth-child(6n + 5):not(.banned) {
background: #fdf2f8;
border-color: #fce7f3;
}
.clientCard:nth-child(6n + 6):not(.banned) {
background: #fefce8;
border-color: #fef08a;
}
/* Avatar Styling */
.avatarContainer {
flex-shrink: 0;
position: relative;
display: flex;
align-items: center;
justify-content: center;
}
@media (max-width: 768px) {
.avatarContainer {
margin-top: 4px; /* Align nicely with the text if align-items: flex-start is used */
}
}
.avatarContainer::before {
content: '';
position: absolute;
top: -4px;
left: -4px;
right: -4px;
bottom: -4px;
border-radius: 50%;
border: 2px solid transparent;
transition: border-color 0.4s ease;
}
@media (min-width: 769px) {
.clientCard:hover .avatarContainer::before {
border-color: rgba(139, 92, 246, 0.2);
}
.clientCard.banned .avatarContainer::before {
border-color: rgba(239, 68, 68, 0.2);
}
}
.infoContainer {
flex-grow: 1;
display: flex;
flex-direction: column;
gap: 6px; /* Reduced gap to prevent cutting off text */
min-width: 0; /* Ensures truncation works within flex children */
}
.headerRow {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
flex-wrap: nowrap;
}
.clientName {
margin: 0;
font-size: 17px;
font-weight: 700;
color: #1e293b; /* Dark slate */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
letter-spacing: -0.01em;
line-height: 1.2;
}
@media (max-width: 768px) {
.clientName {
font-size: 16px;
}
}
.bannedBadge {
background: #fef2f2;
color: #dc2626;
font-size: 10px;
font-weight: 700;
padding: 4px 10px;
border-radius: 9999px;
text-transform: uppercase;
letter-spacing: 0.05em;
border: 1px solid #fecaca;
flex-shrink: 0;
}
/* Contact Details */
.clientDetail, .clientPhone {
margin: 0;
font-size: 13px;
font-weight: 500;
color: #475569; /* Slate 600 */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 8px;
line-height: 1.2;
}
@media (max-width: 768px) {
.clientDetail, .clientPhone {
font-size: 12px;
}
}
/* Beautiful Icon Wrappers */
.iconWrapper {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 6px;
background: #f1f5f9;
color: #94a3b8;
flex-shrink: 0;
transition: all 0.3s ease;
}
@media (min-width: 769px) {
.clientCard:hover .iconWrapper {
background: rgba(139, 92, 246, 0.1);
color: var(--wine);
}
.clientCard.banned:hover .iconWrapper {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
}
}
.paginationWrapper {
padding: 16px 30px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(12px);
border-top: 1px solid rgba(226, 232, 240, 0.6);
display: flex;
justify-content: center;
z-index: 10;
border-bottom-left-radius: 24px;
border-bottom-right-radius: 24px;
}
@media (max-width: 768px) {
.paginationWrapper {
padding: 12px 16px;
border-bottom-left-radius: 16px;
border-bottom-right-radius: 16px;
}
}
.emptyState {
grid-column: 1 / -1;
text-align: center;
padding: 100px 20px;
color: #94a3b8;
font-size: 16px;
font-weight: 500;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
@@ -0,0 +1,246 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import ClientsIcon from "@mui/icons-material/AccountBoxOutlined";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import EmailOutlinedIcon from "@mui/icons-material/EmailOutlined";
import PhoneOutlinedIcon from "@mui/icons-material/PhoneOutlined";
import Avatar from "@components/Avatar/Avatar";
import PaginationControls from "@components/PaginationControls/PaginationControls";
import pageStyle from "./page.module.css";
import { findClientsByOrganization } from "./Clients.Service";
import {
FindClientsParams,
OrganizationClientsResults,
OrganizationClientView,
PaginateOrganizationClientsParams,
} from "@core/Models/Clients.model";
import { useNavigation } from "@hooks/goto";
import { FilterData, useFilterStore } from "@core/Store/Filter.Store";
import Filterable from "@core/app/components/Filterable/Filterable";
import Textbox from "@core/app/components/Textbox/Textbox";
type UrlData = {
id: string;
};
type ClientParams = FilterData & FindClientsParams;
const FilterContent = () => {
const filter = useFilterStore();
const [filterFirstName, setFilterFirstName] = useState<string>("");
const [filterLastName, setFilterLastName] = useState<string>("");
const [filterEmail, setFilterEmail] = useState<string>("");
const onChangeHandler = (field: string, value: string) => {
const data = useFilterStore.getState().filterData as ClientParams;
// Actualizamos el estado local según el campo modificado
switch (field) {
case "firstName":
setFilterFirstName(value);
break;
case "lastName":
setFilterLastName(value);
break;
case "email":
setFilterEmail(value);
break;
}
// Creamos el filtro actualizado
const updatedFilter: ClientParams = {
...data,
[field]: value, // Agregamos o actualizamos el campo
};
// Eliminamos los campos con valores vacíos
const filteredFilter = Object.fromEntries(
Object.entries(updatedFilter).filter(([, v]) => v && v.trim() !== "")
);
filter.setFilterData(filteredFilter as ClientParams);
};
return (
<>
<Textbox
placeholder="Nombre"
width="100%"
value={filterFirstName}
type="text"
onChange={(e) => {
onChangeHandler("firstName", e.target.value);
}}
/>
<Textbox
placeholder="Apellido"
width="100%"
value={filterLastName}
type="text"
onChange={(e) => {
onChangeHandler("lastName", e.target.value);
}}
/>
<Textbox
placeholder="Email"
width="100%"
value={filterEmail}
type="text"
onChange={(e) => {
onChangeHandler("email", e.target.value);
}}
/>
</>
);
};
export type ClientRow = OrganizationClientView;
export default function ClientsGrid() {
const data = useParams<UrlData>();
const oid = data.id;
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const filter = useFilterStore();
const { goTo } = useNavigation();
const [pageSize] = useState<number>(
parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE
: "10"
)
);
const [clients, setClients] = useState<OrganizationClientsResults>();
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
filter.setTitle("Filtrar Clientes");
filter.clearFilterData();
filter.setContent(<FilterContent />);
filter.setOnFilter(() => {
loadClients(1, pageSize);
});
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Mis organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + oid,
title: "Organizacion",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Clientes",
icon: <ClientsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
loadClients(1, pageSize);
}, []);
const loadClients = (page: number = 1, pageSize: number = 30) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const data = useFilterStore.getState().filterData;
const filterData: PaginateOrganizationClientsParams = {
companyId: oid,
page: page,
limit: pageSize,
...data,
};
findClientsByOrganization(filterData)
.then((res) => {
setClients(res);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
};
const handleClientClick = (client: ClientRow) => {
goTo("/admin/org/" + oid + "/client/" + client.id + "/profile");
};
return (
<>
<Filterable />
<Location />
<div className={pageStyle.pageContainer}>
{clients?.data && clients.data.length > 0 ? (
<div className={pageStyle.clientsGrid}>
{clients.data.map((client) => (
<div
key={client.id}
className={`${pageStyle.clientCard} ${client.banned ? pageStyle.banned : ''}`}
onClick={() => handleClientClick(client as ClientRow)}
>
<div className={pageStyle.avatarContainer}>
<Avatar
src={client.avatar}
alt={client.fullName}
size="small"
border="none"
/>
</div>
<div className={pageStyle.infoContainer}>
<div className={pageStyle.headerRow}>
<h3 className={pageStyle.clientName}>{client.fullName}</h3>
{client.banned && (
<span className={pageStyle.bannedBadge}>Baneado</span>
)}
</div>
<p className={pageStyle.clientDetail}>
<div className={pageStyle.iconWrapper}>
<EmailOutlinedIcon sx={{ fontSize: 14 }} />
</div>
{client.email || "Sin email"}
</p>
<p className={pageStyle.clientPhone}>
<div className={pageStyle.iconWrapper}>
<PhoneOutlinedIcon sx={{ fontSize: 14 }} />
</div>
{client.phoneAreaCode && client.phoneNumber
? `${client.phoneCountryCode || ''} ${client.phoneAreaCode} - ${client.phoneNumber}`
: "Sin teléfono"
}
</p>
</div>
</div>
))}
</div>
) : (
<div className={pageStyle.emptyState}>
No se encontraron clientes con los filtros aplicados.
</div>
)}
{clients && clients.pages > 1 && (
<div className={pageStyle.paginationWrapper}>
<PaginationControls
current={clients.page}
size={pageSize}
max={clients.pages}
onChange={(page, size) => loadClients(page, size)}
/>
</div>
)}
</div>
</>
);
}
@@ -0,0 +1,130 @@
import ApiRequest from "@services/Api.Service";
import {
CompanyEmployeesView,
FindEmployeesParams,
CreateEmployeeParams,
IEmployee,
ValidateEmployeeParams,
FindEmployeesByIdParams,
UpdateEmployeeRolesParams,
FindCollaboratorServicesParams,
CollaboratorServiceView,
UpdateCollaboratorServicesParams,
FindSchedulesParams,
CollaboratorSchedulesView,
UpdateScheduleParams,
UpdateEmployeeParams,
} from "@models/Collaborators.model";
import { CollaboratorView, FindCollaboratorByEmailParams } from "@models/User.model";
import { ApiVoidResult } from "@models/Api.VoidResult.type";
import {
CreateSchedulesDisabledParams,
DeleteSchedulesDisabledParams,
FindSchedulesDisabledParams,
SchedulesDisabledByCollaboratorView,
} from "@core/Models/SchedulesDisabled";
import {
FindSchedulesOverridesParams,
CreateSchedulesOverridesParams,
UpdateSchedulesOverridesParams,
DeleteSchedulesOverridesParams,
ISchedulesOverrides,
} from "@core/Models/SchedulesOverrides";
export const loadCollaboratorsByCompanyId = async (
data: FindEmployeesParams
): Promise<CompanyEmployeesView[]> => {
return ApiRequest.post<CompanyEmployeesView[]>("employees/get-by-company", data);
};
export const findCollaboratorsByEmail = async (
data: FindCollaboratorByEmailParams
): Promise<CollaboratorView | null> => {
return ApiRequest.post<CollaboratorView | null>("users/get-by-email", data);
};
export const findCollaboratorsById = async (
data: FindEmployeesByIdParams
): Promise<CompanyEmployeesView | null> => {
return ApiRequest.post<CompanyEmployeesView | null>("employees/get-by-id", data);
};
export const findCollaboratorsServices = async (
data: FindCollaboratorServicesParams
): Promise<CollaboratorServiceView | null> => {
return ApiRequest.post<CollaboratorServiceView | null>("employees/get-all-services", data);
};
export const findCollaboratorsSchedules = async (
data: FindSchedulesParams
): Promise<CollaboratorSchedulesView> => {
return ApiRequest.post<CollaboratorSchedulesView>("schedules/get-all-by-collaborator", data);
};
export const updateCollaboratorsServices = async (
data: UpdateCollaboratorServicesParams
): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("employees/update-collaborator-services", data);
};
export const updateCollaboratorsSchedules = async (data: UpdateScheduleParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("schedules/update", data);
};
export const createCollaborator = async (data: CreateEmployeeParams): Promise<IEmployee> => {
return ApiRequest.post<IEmployee>("employees/create", data);
};
export const validateGuestCollaborator = async (data: ValidateEmployeeParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("employees/validate-guest", data);
};
export const validateHostCollaborator = async (data: ValidateEmployeeParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("employees/validate-host", data);
};
export const updateCollaboratorRoles = async (data: UpdateEmployeeRolesParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("employees/update-roles", data);
};
export const updateCollaborator = async (data: UpdateEmployeeParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("employees/update", data);
};
export const findSchedulesDisabledByCollaborator = async (
data: FindSchedulesDisabledParams
): Promise<SchedulesDisabledByCollaboratorView> => {
return ApiRequest.post<SchedulesDisabledByCollaboratorView>(
"schedules-disabled/find-by-collaborator",
data
);
};
export const createScheduleDisabled = async (data: CreateSchedulesDisabledParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("schedules-disabled/create", data);
};
export const deleteScheduleDisabled = async (data: DeleteSchedulesDisabledParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("schedules-disabled/delete", data);
};
export const getSchedulesOverrides = async (
data: FindSchedulesOverridesParams
): Promise<ISchedulesOverrides[]> => {
return ApiRequest.post<ISchedulesOverrides[]>(
"schedules-overrides/get",
data
);
};
export const createSchedulesOverrides = async (data: CreateSchedulesOverridesParams): Promise<ISchedulesOverrides> => {
return ApiRequest.post<ISchedulesOverrides>("schedules-overrides/create", data);
};
export const updateSchedulesOverrides = async (data: UpdateSchedulesOverridesParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("schedules-overrides/update", data);
};
export const deleteSchedulesOverrides = async (data: DeleteSchedulesOverridesParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("schedules-overrides/delete", data);
};
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function CreateDiscountsLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,174 @@
"use client";
import { useState, useEffect } from "react";
import { useParams, useSearchParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import AutoDeleteIcon from "@mui/icons-material/AutoDeleteOutlined";
import dayjs from "dayjs";
import "dayjs/locale/es";
import TextTime from "@core/app/components/TextTime/TextTime";
import TextDate from "@core/app/components/TextDate/TextDate";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import { createScheduleDisabled } from "../../../Collaborators.Service";
import MaterialAlert from "@mui/material/Alert";
dayjs.locale("es");
type UrlData = {
id: string;
cid: string;
};
export default function DiscountCreate() {
const data = useParams<UrlData>();
const id = data.id;
const cid = data.cid;
const searchParams = useSearchParams();
const cname = searchParams.get("cname");
const alert = useAlert();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const [dateFromDay, setDateFromDay] = useState<dayjs.Dayjs>(dayjs().startOf("day"));
const [dateFromTime, setDateFromTime] = useState<dayjs.Dayjs>(dayjs().hour(8).startOf("hour"));
const [dateToDay, setDateToDay] = useState<dayjs.Dayjs>(dayjs().startOf("day"));
const [dateToTime, setDateToTime] = useState<dayjs.Dayjs>(dayjs().hour(20).startOf("hour"));
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/collaborators" + "/" + cid,
title: `${cname}`,
icon: <AccountCircleOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators/" + cid + "/disabled",
title: "Restricciones",
icon: <AutoDeleteIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Crear",
icon: <AutoDeleteIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
const dateFromData = dayjs(
dateFromDay.format("YYYY-MM-DD") + " " + dateFromTime.format("HH:mm:ss")
);
const dateToData = dayjs(dateToDay.format("YYYY-MM-DD") + " " + dateToTime.format("HH:mm:ss"));
createScheduleDisabled({
employeeId: cid,
companyId: id,
startDate: dateFromData.toDate(),
endDate: dateToData.toDate(),
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess(`La restricción ha sido creada`);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
setTimeout(() => {
goTo("/admin/org/profile/" + id + "/collaborators/" + cid + "/disabled");
}, 2000);
})
.catch((error) => {
alert.showError(error.format());
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
});
}
}, [eventHandler.eventType]);
return (
<>
<Location />
<MaterialAlert
severity="info"
sx={{
marginTop: "10px",
maxWidth: "680px",
marginLeft: "auto",
marginRight: "auto",
}}
>
<strong>Atención:</strong> El periodo de tiempo cargado hará que el colaborador no pueda
recibir turnos durante el mismo.
</MaterialAlert>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "center",
gap: "10px",
marginTop: "15px",
}}
>
<div>
<TextDate
placeholder="Desde"
selectedDate={dateFromDay}
onChange={(value: dayjs.Dayjs) => {
setDateFromDay(value);
}}
/>
</div>
<div>
<TextTime
placeholder="&nbsp;"
width="100%"
timeFraction={1}
value={dateFromTime}
onChange={setDateFromTime}
/>
</div>
</div>
<div
style={{
display: "flex",
flexDirection: "row",
justifyContent: "center",
gap: "10px",
marginTop: "15px",
}}
>
<div>
<TextDate
placeholder="Hasta"
selectedDate={dateToDay}
onChange={(value: dayjs.Dayjs) => {
setDateToDay(value);
}}
/>
</div>
<div>
<TextTime
placeholder="&nbsp;"
width="100%"
timeFraction={1}
value={dateToTime}
onChange={setDateToTime}
/>
</div>
</div>
<div style={{ marginTop: "15px" }}></div>
</>
);
}
@@ -0,0 +1,203 @@
"use client";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import AutoDeleteIcon from "@mui/icons-material/AutoDeleteOutlined";
import MoreTimeIcon from "@mui/icons-material/MoreTime";
import { useAlert } from "@store/Alert.Store";
import { useConfirmStore } from "@store/Confirm.Store";
import { useEffect, useState } from "react";
import { deleteScheduleDisabled, findSchedulesDisabledByCollaborator } from "../../Collaborators.Service";
import dayjs from "dayjs";
import "dayjs/locale/es";
import { SchedulesDisabledByCollaboratorView } from "@core/Models/SchedulesDisabled";
import { useNavigation } from "@hooks/goto";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
import ButtonMaterial from "@mui/material/Button";
import TimeMarker from "@core/app/components/TimeMarker/TimeMarker";
import MaterialAlert from "@mui/material/Alert";
dayjs.locale("es");
type UrlData = {
id: string;
cid: string;
};
export default function CollaboratorDisabled() {
const data = useParams<UrlData>();
const id = data.id;
const cid = data.cid;
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const confirm = useConfirmStore();
const alert = useAlert();
const { goTo } = useNavigation();
const bottomToolbar = useBottomToolbarStore();
const [disabledItems, setDisabledItems] = useState<SchedulesDisabledByCollaboratorView>();
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
if (!disabledItems) {
return;
}
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Colaboradores",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators" + "/" + cid,
title: disabledItems.employeeFullName,
icon: <AccountCircleOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Restricciones",
icon: <AutoDeleteIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, [disabledItems]);
useEffect(() => {
loadDisabledSchedules();
}, [id, cid]);
useEffect(() => {
bottomToolbar.set(
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
fontWeight: "bold",
color: "var(--white)",
padding: "10px",
width: "100%",
fontSize: "22px",
paddingRight: "50px",
}}
>
<ButtonMaterial
variant="text"
startIcon={<MoreTimeIcon />}
sx={{ color: "var(--white)" }}
onClick={() => {
handleAddItem();
}}
>
Nuevo
</ButtonMaterial>
</div>
);
return () => {
bottomToolbar.clear();
};
}, [disabledItems]);
const loadDisabledSchedules = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
findSchedulesDisabledByCollaborator({
companyId: id,
employeeId: cid,
sessionUser: SessionInfo.userId,
})
.then((items) => {
if (items === null) {
return;
}
setDisabledItems(items);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const handleAddItem = () => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
if (!disabledItems) {
return;
}
goTo(
"/admin/org/profile/" +
id +
"/collaborators/" +
cid +
"/disabled/new?cname=" +
encodeURIComponent(disabledItems.employeeFullName)
);
};
const handleDeleteItem = (itemId: string) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
deleteScheduleDisabled({
companyId: id,
id: itemId,
sessionUser: SessionInfo.userId,
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
loadDisabledSchedules();
});
};
return (
<>
<Location />
<MaterialAlert
severity="info"
sx={{
marginTop: "10px",
maxWidth: "680px",
marginLeft: "auto",
marginRight: "auto",
}}
>
<strong>Atención:</strong> Los periodos de tiempo listados abajo son los que se encuentran
deshabilitados para el colaborador. Esto significa que no se le asignarán turnos durante estos
periodos de tiempo.
</MaterialAlert>
<div style={{ display: "flex", flexDirection: "column", marginTop: "20px", gap: "10px" }}>
{disabledItems?.disabledItems.map((item, index) => (
<TimeMarker
key={index}
start={dayjs(item.startDate)}
end={dayjs(item.endDate)}
startColor={"#4b43bdff"}
endColor={"#EF4444"}
onLongPress={() => {
confirm.show(
"¿Está seguro que desea eliminar esta restricción?",
() => {
handleDeleteItem(item._id);
},
() => {}
);
}}
/>
))}
</div>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function CollaboratorConfigLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,125 @@
"use client";
import { useParams } from "next/navigation";
import { useState, useEffect } from "react";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useAlert } from "@store/Alert.Store";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import LocalPoliceOutlinedIcon from "@mui/icons-material/LocalPoliceOutlined";
import MaterialAlert from "@mui/material/Alert";
import { PopoverPicker } from "@core/app/components/ColorPicker/ColorPicker";
import { findCollaboratorsById, updateCollaborator } from "../../Collaborators.Service";
import { ApiError } from "@models/Server.Error.model";
import { UpdateEmployeeParams } from "@models/Collaborators.model";
type UrlData = {
id: string;
cid: string;
};
export default function OrganizationEdit() {
const data = useParams<UrlData>();
const id = data.id;
const cid = data.cid;
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
const [collaboratorName, setCollaboratorName] = useState<string>("");
const [collaboratorColor, setCollaboratorColor] = useState<string>("");
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Colaboradores",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators" + "/" + cid,
title: collaboratorName,
icon: <AccountCircleOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Editar",
icon: <LocalPoliceOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
eventHandler.setEventType(EVENT_TYPES.LOADING);
loadCollaborator();
}, [collaboratorName]);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const data: UpdateEmployeeParams = {
id: cid,
calendarColor: collaboratorColor,
sessionUser: SessionInfo.userId,
};
updateCollaborator(data)
.then(() => {
alert.showSuccess("Información actualizada!");
})
.catch((error: ApiError) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
const loadCollaborator = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
findCollaboratorsById({ id: cid })
.then((collaborator) => {
if (collaborator === null) {
return;
}
setCollaboratorName(collaborator.fullName);
setCollaboratorColor(collaborator.calendarColor);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
return (
<>
<Location />
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Color del calendario:</b> el color será utilizado para representar los turnos de
este colaborador en el calendario.
</MaterialAlert>
<PopoverPicker
placeholder="Color del calendario"
color={collaboratorColor}
onChange={(color: string) => {
setCollaboratorColor(color);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,5 @@
"use client";
export default function CollaboratorLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function CreateOverridesLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,244 @@
"use client";
import { useParams, useSearchParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import EventAvailableOutlinedIcon from "@mui/icons-material/EventAvailableOutlined";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import { useAlert } from "@store/Alert.Store";
import { useConfirmStore } from "@store/Confirm.Store";
import { useEffect, useState } from "react";
import MaterialAlert from "@mui/material/Alert";
import { ScheduleItem } from "@core/Models/Collaborators.model";
import { createSchedulesOverrides } from "../../../Collaborators.Service";
import { Grid2 } from "@mui/material";
import TextTime from "@core/app/components/TextTime/TextTime";
import TextDate from "@core/app/components/TextDate/TextDate";
import ScheduleItemRow from "@components/ScheduleItemRow/ScheduleItemRow";
import Button from "@core/app/components/Button/Button";
import dayjs from "dayjs";
import "dayjs/locale/es";
import { useNavigation } from "@hooks/goto";
dayjs.locale("es");
type UrlData = {
id: string;
cid: string;
};
interface SchedulesDateItem {
from: dayjs.Dayjs;
to: dayjs.Dayjs;
data: ScheduleItem;
}
export default function OverridesCreate() {
const data = useParams<UrlData>();
const id = data.id;
const cid = data.cid;
const searchParams = useSearchParams();
const cname = searchParams.get("cname");
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const confirm = useConfirmStore();
const alert = useAlert();
const { goTo } = useNavigation();
const [overrideDate, setOverrideDate] = useState<dayjs.Dayjs>(dayjs(new Date()).startOf("day"));
const [from, setFrom] = useState<dayjs.Dayjs>(dayjs(new Date()).hour(8).minute(0));
const [to, setTo] = useState<dayjs.Dayjs>(dayjs(new Date()).hour(12).minute(0));
const [schedules, setSchedules] = useState<Array<SchedulesDateItem>>([]);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/collaborators" + "/" + cid,
title: `${cname}`,
icon: <AccountCircleOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators/" + cid + "/overrides",
title: "Excepciones",
icon: <EventAvailableOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Crear",
icon: <EventAvailableOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
const dateToSave = overrideDate.toDate();
createSchedulesOverrides({
companyId: id,
employeeId: cid,
date: dateToSave,
schedules: getSchedulesToSave(),
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("Excepción de horario creada");
eventHandler.setEventType(EVENT_TYPES.SLEEP);
setTimeout(() => {
goTo("/admin/org/profile/" + id + "/collaborators/" + cid + "/overrides");
}, 2000);
})
.catch((error) => {
alert.showError(error.format());
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
});
}
}, [eventHandler.eventType]);
const getSchedulesToSave = (): ScheduleItem[] => {
return schedules.map((schedule) => {
return {
from: schedule.from.format("HH:mm"),
to: schedule.to.format("HH:mm"),
disabled: schedule.data.disabled || false,
};
});
};
const addButtonHandler = () => {
const newSchedule: SchedulesDateItem = {
from: from.clone(),
to: to.clone(),
data: {
from: from.format("HH:mm"),
to: to.format("HH:mm"),
disabled: false,
},
};
const hasOverlap = schedules.some((schedule) => {
const existingFrom = dayjs(schedule.from);
const existingTo = dayjs(schedule.to);
return newSchedule.from.isBefore(existingTo) && newSchedule.to.isAfter(existingFrom);
});
if (hasOverlap) {
alert.showError(
"El horario se solapa con uno existente. Por favor, elige otro horario."
);
} else if (newSchedule.from.isAfter(newSchedule.to)) {
alert.showError("La hora de inicio debe ser anterior a la hora de fin.");
} else {
setSchedules([...schedules, newSchedule]);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}
};
const deleteScheduleHandler = (from: string) => {
confirm.show(
"¿Estas seguro que queres eliminar el horario?",
() => {
const filteredSchedules = schedules.filter(
(schedule) => schedule.data.from != from
);
setSchedules(filteredSchedules);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
},
() => { }
);
};
const checkChangeHandler = (from: string, data: boolean) => {
const newSchedules = schedules.map((schedule) => {
if (schedule.data.from == from) {
return {
...schedule,
data: {
...schedule.data,
disabled: data,
},
};
}
return schedule;
});
setSchedules(newSchedules);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
};
return (
<>
<Location />
<div
style={{ display: "flex", flexDirection: "column", marginTop: "20px", gap: "10px", maxWidth: "680px", marginLeft: "auto", marginRight: "auto" }}
>
<div style={{ marginBottom: "15px" }}>
<TextDate
placeholder="Fecha de la Excepción"
selectedDate={overrideDate}
onChange={(date) => {
setOverrideDate(date);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</div>
<Grid2 container sx={{ marginTop: "0px" }}>
<Grid2 size={3}>
<TextTime
placeholder="Desde"
width="100%"
value={from}
onChange={setFrom}
/>
</Grid2>
<Grid2 size={1}></Grid2>
<Grid2 size={3}>
<TextTime placeholder="Hasta" width="100%" value={to} onChange={setTo} />
</Grid2>
<Grid2 size={1}></Grid2>
<Grid2
size={4}
sx={{ display: "flex", justifyContent: "center", alignItems: "end" }}
>
<Button
text="Agregar Horario"
width="100%"
color="primary"
style={{ height: "42px" }}
onClick={addButtonHandler}
/>
</Grid2>
</Grid2>
{schedules.map((schedule, index) => (
<ScheduleItemRow
key={index}
from={schedule.data.from}
to={schedule.data.to}
disabled={schedule.data.disabled}
onCheckChange={checkChangeHandler}
onDelete={deleteScheduleHandler}
/>
))}
{schedules.length < 1 && (
<MaterialAlert sx={{ width: "100%" }} severity="warning">
<strong>Atención:</strong> Si guardas esta excepción sin cargar ningún horario,
el colaborador no tendrá turnos disponibles en todo el día, reemplazando su horario normal.
</MaterialAlert>
)}
</div>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,244 @@
"use client";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import EventAvailableOutlinedIcon from "@mui/icons-material/EventAvailableOutlined";
import MoreTimeIcon from "@mui/icons-material/MoreTime";
import { useAlert } from "@store/Alert.Store";
import { useConfirmStore } from "@store/Confirm.Store";
import { useEffect, useState } from "react";
import {
deleteSchedulesOverrides,
getSchedulesOverrides,
findCollaboratorsById,
} from "../../Collaborators.Service";
import dayjs from "dayjs";
import "dayjs/locale/es";
import { ISchedulesOverrides } from "@core/Models/SchedulesOverrides";
import { useNavigation } from "@hooks/goto";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
import ButtonMaterial from "@mui/material/Button";
import MaterialAlert from "@mui/material/Alert";
import { CompanyEmployeesView } from "@core/Models/Collaborators.model";
dayjs.locale("es");
type UrlData = {
id: string;
cid: string;
};
export default function CollaboratorOverrides() {
const data = useParams<UrlData>();
const id = data.id;
const cid = data.cid;
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const confirm = useConfirmStore();
const alert = useAlert();
const { goTo } = useNavigation();
const bottomToolbar = useBottomToolbarStore();
const [overrides, setOverrides] = useState<ISchedulesOverrides[]>([]);
const [collaborator, setCollaborator] = useState<CompanyEmployeesView>();
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
loadCollaborator();
}, [id, cid]);
useEffect(() => {
location.clear();
if (!collaborator) {
return;
}
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Colaboradores",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators" + "/" + cid,
title: collaborator.fullName,
icon: <AccountCircleOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Excepciones",
icon: <EventAvailableOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, [collaborator]);
useEffect(() => {
loadOverrides();
}, [id, cid]);
useEffect(() => {
bottomToolbar.set(
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
fontWeight: "bold",
color: "var(--white)",
padding: "10px",
width: "100%",
fontSize: "22px",
paddingRight: "50px",
}}
>
<ButtonMaterial
variant="text"
startIcon={<MoreTimeIcon />}
sx={{ color: "var(--white)" }}
onClick={() => {
handleAddItem();
}}
>
Nuevo
</ButtonMaterial>
</div>
);
return () => {
bottomToolbar.clear();
};
}, [overrides, collaborator]);
const loadCollaborator = () => {
findCollaboratorsById({ id: cid })
.then((data) => {
if (data) setCollaborator(data);
});
};
const loadOverrides = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
getSchedulesOverrides({
companyId: id,
employeeId: cid,
})
.then((items) => {
if (items === null) {
return;
}
setOverrides(items);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const handleAddItem = () => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
if (!collaborator) {
return;
}
goTo(
"/admin/org/profile/" +
id +
"/collaborators/" +
cid +
"/overrides/new?cname=" +
encodeURIComponent(collaborator.fullName)
);
};
const handleDeleteItem = (itemId: string) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
deleteSchedulesOverrides({
companyId: id,
id: itemId,
sessionUser: SessionInfo.userId,
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
loadOverrides();
});
};
return (
<>
<Location />
<MaterialAlert
severity="info"
sx={{
marginTop: "10px",
maxWidth: "680px",
marginLeft: "auto",
marginRight: "auto",
}}
>
<strong>Excepciones de horario:</strong> Las fechas configuradas a continuación reemplazarán
el horario habitual del colaborador. Resulta útil para feriados o días especiales donde el profesional
decide trabajar en otro horario o no trabajar (si se definen sin horarios activos).
</MaterialAlert>
<div style={{ display: "flex", flexDirection: "column", marginTop: "20px", gap: "10px", maxWidth: "680px", marginLeft: "auto", marginRight: "auto" }}>
{overrides.map((item, index) => (
<div key={index} style={{
display: 'flex',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: '10px',
border: '1px solid #ccc',
borderRadius: '5px',
backgroundColor: '#fff'
}}>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<strong style={{ fontSize: '1.1rem' }}>{dayjs(item.date).format("dddd DD [de] MMMM, YYYY")}</strong>
{(!item.schedules || item.schedules.length === 0) ? (
<span style={{ color: '#EF4444' }}>Sin horarios disponibles</span>
) : (
<span>
{item.schedules.map(sch => (
<span key={String(sch.from)} style={{ marginRight: '10px', color: sch.disabled ? '#EF4444' : '#4b43bdff' }}>
{sch.disabled ? <s>{String(sch.from)} - {String(sch.to)}</s> : `${String(sch.from)} - ${String(sch.to)}`}
</span>
))}
</span>
)}
</div>
<ButtonMaterial
variant="outlined"
color="error"
size="small"
onClick={() => {
confirm.show(
"¿Está seguro que desea eliminar esta excepción?",
() => {
handleDeleteItem(item._id || item.id || "");
},
() => {}
);
}}
>
Eliminar
</ButtonMaterial>
</div>
))}
</div>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,251 @@
"use client";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import { useEffect, useState } from "react";
import { findCollaboratorsById, validateHostCollaborator } from "../Collaborators.Service";
import Avatar from "@components/Avatar/Avatar";
import { CompanyEmployeesView } from "@models/Collaborators.model";
import MaterialAlert from "@mui/material/Alert";
import Button from "@components/Button/Button";
import ButtonPanel, { ButtonPanelItem } from "@core/app/components/ButtonPanel/ButtonPanel";
import SaveAsOutlinedIcon from "@mui/icons-material/SaveAsOutlined";
import TodayOutlinedIcon from "@mui/icons-material/TodayOutlined";
import AssignmentIndOutlinedIcon from "@mui/icons-material/AssignmentIndOutlined";
import CategoryOutlinedIcon from "@mui/icons-material/CategoryOutlined";
import DateRangeOutlinedIcon from "@mui/icons-material/DateRangeOutlined";
import AutoDeleteIcon from '@mui/icons-material/AutoDeleteOutlined';
import EventAvailableOutlinedIcon from '@mui/icons-material/EventAvailableOutlined';
import dayjs from "dayjs";
import "dayjs/locale/es";
dayjs.locale("es");
type UrlData = {
id: string;
cid: string;
};
export default function OrganizationService() {
const data = useParams<UrlData>();
const id = data.id;
const cid = data.cid;
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const alert = useAlert();
const { goTo } = useNavigation();
const [collaborator, setCollaborator] = useState<CompanyEmployeesView>();
const [collaboratorName, setCollaboratorName] = useState<string>("");
const [, setCollaboratorUserId] = useState<string>("");
const [collaboratorAvatar, setCollaboratorAvatar] = useState<string>("");
const buttons: Array<ButtonPanelItem> = [
{
icon: <SaveAsOutlinedIcon />,
text: "Editar",
onClick: () => {
goTo("/admin/org/profile/" + id + "/collaborators/" + cid + "/edit");
},
},
{
icon: <TodayOutlinedIcon />,
text: "Turnos",
onClick: () => {
goTo(
"/admin/org/profile/" +
id +
"/appointments/" +
encodeURIComponent(dayjs(new Date()).clone().startOf("day").toISOString())
);
},
},
{
icon: <AssignmentIndOutlinedIcon />,
text: "Roles",
onClick: () => {
goTo("/admin/org/profile/" + id + "/collaborators/" + cid + "/roles");
},
},
{
icon: <CategoryOutlinedIcon />,
text: "Servicios",
onClick: () => {
goTo("/admin/org/profile/" + id + "/collaborators/" + cid + "/services");
},
},
{
icon: <DateRangeOutlinedIcon />,
text: "Horarios",
onClick: () => {
goTo("/admin/org/profile/" + id + "/collaborators/" + cid + "/schedules");
},
},
{
icon: <AutoDeleteIcon />,
text: "Restricciones",
onClick: () => {
goTo("/admin/org/profile/" + id + "/collaborators/" + cid + "/disabled");
},
},
{
icon: <EventAvailableOutlinedIcon />,
text: "Excepciones",
onClick: () => {
goTo("/admin/org/profile/" + id + "/collaborators/" + cid + "/overrides");
},
},
];
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id,
title: "Organizacion",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Colaboradores",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/services",
title: collaboratorName,
icon: (
<Avatar
name={collaboratorName}
size="super-small"
border="none"
style={{ marginRight: "10px" }}
src={collaboratorAvatar}
alt={collaboratorName}
/>
),
});
}, [collaboratorName]);
useEffect(() => {
loadCollaborator();
}, []);
const loadCollaborator = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
findCollaboratorsById({ id: cid })
.then((collaborator) => {
if (collaborator === null) {
return;
}
setCollaborator(collaborator);
setCollaboratorName(collaborator.fullName);
setCollaboratorUserId(collaborator.userId);
setCollaboratorAvatar(collaborator.avatar);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const handleAgree = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
validateHostCollaborator({
employeeId: cid,
sessionUser: SessionInfo.userId,
})
.then(() => {
loadCollaborator();
})
.catch((error) => {
alert.showError(error.format());
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
return (
<>
<Location />
<div
style={{ display: "flex", flexDirection: "column", marginTop: "20px", gap: "10px" }}
>
{collaborator && !collaborator.hostOk && (
<div
style={{
display: "flex",
flexDirection: "column",
alignContent: "center",
justifyContent: "center",
width: "100%",
}}
>
<MaterialAlert
severity="info"
sx={{
marginTop: "10px",
maxWidth: "680px",
marginLeft: "auto",
marginRight: "auto",
}}
>
<strong>Atención:</strong> Para que el colaborador pueda formar parte de
la organización primero debes confirmar y esperar a que el invitado
acepte la vinculación a la organización.
</MaterialAlert>
<Button
name="acceptButton"
width="100%"
text="Confirmar colaborador"
style={{
marginTop: "20px",
maxWidth: "680px",
marginLeft: "auto",
marginRight: "auto",
}}
onClick={handleAgree}
/>
</div>
)}
{collaborator && collaborator.hostOk && !collaborator.fullOk && (
<>
<MaterialAlert
severity="info"
sx={{
marginTop: "10px",
maxWidth: "680px",
marginLeft: "auto",
marginRight: "auto",
}}
>
<strong>Atención:</strong> Para que el colaborador pueda formar parte de
la organización debes esperar a que el invitado acepte la vinculación a
la organización.
</MaterialAlert>
</>
)}
{collaborator && collaborator.fullOk && <ButtonPanel items={buttons} />}
</div>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,5 @@
"use client";
export default function CollaboratorRolesLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
@@ -0,0 +1,198 @@
"use client";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import LocalPoliceOutlinedIcon from "@mui/icons-material/LocalPoliceOutlined";
import { useAlert } from "@store/Alert.Store";
import { useEffect, useState } from "react";
import { findCollaboratorsById, updateCollaboratorRoles } from "../../Collaborators.Service";
import { CompanyEmployeesView, EmployeeRoles } from "@models/Collaborators.model";
import MaterialAlert from "@mui/material/Alert";
import TriSwitch from "@core/app/components/TriSwitch/TriSwitch";
type UrlData = {
id: string;
cid: string;
};
export default function OrganizationService() {
const data = useParams<UrlData>();
const id = data.id;
const cid = data.cid;
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const alert = useAlert();
const [isFirstRender, setIsFirstRender] = useState(true);
const [role, setRole] = useState<string>("");
const [rolesIndex, setRolesIndex] = useState<Map<string, string>>(new Map<string, string>());
const [collaborator, setCollaborator] = useState<CompanyEmployeesView>();
const [collaboratorName, setCollaboratorName] = useState<string>("");
const [, setCollaboratorUserId] = useState<string>("");
const [, setCollaboratorAvatar] = useState<string>("");
useEffect(() => {
const rolesIndexMap = new Map<string, string>();
rolesIndexMap.set("Administrador", "admin,employee");
rolesIndexMap.set("Colaborador", "employee");
setRolesIndex(rolesIndexMap);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
if (isFirstRender) {
setIsFirstRender(false);
} else {
handleRoleChange(); // Solo se ejecutará después de la primera vez
}
}, [role]);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Colaboradores",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators" + "/" + cid,
title: collaboratorName,
icon: <AccountCircleOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Rol del colaborador",
icon: <LocalPoliceOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, [collaboratorName]);
useEffect(() => {
loadCollaborator();
}, []);
const loadCollaborator = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
findCollaboratorsById({ id: cid })
.then((collaborator) => {
if (collaborator === null) {
return;
}
setCollaborator(collaborator);
setCollaboratorName(collaborator.fullName);
setCollaboratorUserId(collaborator.userId);
setCollaboratorAvatar(collaborator.avatar);
setRole(getRoleValue(collaborator.roles));
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const getRolesByName = (name: EmployeeRoles): EmployeeRoles[] => {
const index = rolesIndex.get(name);
if (!index) {
return [];
}
const returnRoles: EmployeeRoles[] = [];
index.split(",").forEach((role) => {
returnRoles.push(role as EmployeeRoles);
});
return returnRoles;
};
const getRoleValue = (roles: EmployeeRoles[] | undefined): string => {
if (!roles) {
return "";
}
if (roles.includes(EmployeeRoles.ADMIN)) {
return "Administrador";
} else if (roles.includes(EmployeeRoles.EMPLOYEE)) {
return "Colaborador";
} else {
return "";
}
};
const handleRoleChange = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
if (!collaborator) {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
return;
}
const dbRole = getRoleValue(collaborator.roles);
if (dbRole === role) {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
return;
}
updateCollaboratorRoles({
employeeId: cid,
companyId: id,
sessionUser: SessionInfo.userId,
roles: getRolesByName(role as EmployeeRoles),
})
.finally(() => {
alert.showSuccess("Rol actualizado");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
return (
<>
<Location />
<div
style={{ display: "flex", flexDirection: "column", marginTop: "20px", gap: "10px" }}
>
<MaterialAlert sx={{ marginBottom: "10px" }} severity="info">
<p>
Desde aquí puedes gestionar el rol de un colaborador, asignando permisos
avanzados con el rol de Administrador o permisos básicos con el rol de
Colaborador.
</p>
<p>
<strong>Administrador:</strong> Puede hacer todo lo que hace un colaborador,
pero tambien puede modificar el perfil de la organización y todo lo
relacionado con ella.
</p>
<p>
<strong>Colaborador:</strong> Los colaboradores pueden definir sus horarios,
administrar sus turnos, responder sus mensajes.
</p>
</MaterialAlert>
<TriSwitch
option1="Administrador"
option2="Colaborador"
value={role}
onChange={(value: string) => {
setRole(value);
}}
/>
</div>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function CollaboratorSchedulesLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,311 @@
"use client";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import SchedulesIcon from "@mui/icons-material/AccessTime";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import { useAlert } from "@store/Alert.Store";
import { useConfirmStore } from "@store/Confirm.Store";
import { useEffect, useState } from "react";
import MaterialAlert from "@mui/material/Alert";
import { CollaboratorSchedulesView, ScheduleItem } from "@core/Models/Collaborators.model";
import {
findCollaboratorsSchedules,
updateCollaboratorsSchedules,
} from "../../Collaborators.Service";
import WeekDaySelector from "@core/app/components/WeekDaySelector/WeekDaySelector";
import { Grid2 } from "@mui/material";
import TextTime from "@core/app/components/TextTime/TextTime";
import ScheduleItemRow from "@components/ScheduleItemRow/ScheduleItemRow";
import Button from "@core/app/components/Button/Button";
import dayjs from "dayjs";
import "dayjs/locale/es";
dayjs.locale("es");
type UrlData = {
id: string;
cid: string;
};
interface SchedulesDateItem {
from: dayjs.Dayjs;
to: dayjs.Dayjs;
data: ScheduleItem;
}
export default function CollaboratorSchedules() {
const data = useParams<UrlData>();
const id = data.id;
const cid = data.cid;
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const confirm = useConfirmStore();
const alert = useAlert();
const [collaborator, setCollaborator] = useState<CollaboratorSchedulesView>();
const [weekDay, setWeekDay] = useState(1);
const [from, setFrom] = useState<dayjs.Dayjs>(dayjs(new Date()).hour(8).minute(0));
const [to, setTo] = useState<dayjs.Dayjs>(dayjs(new Date()).hour(12).minute(0));
const [schedules, setSchedules] = useState<Array<SchedulesDateItem>>([]);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
if (!collaborator) {
return;
}
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Colaboradores",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators" + "/" + cid,
title: collaborator.fullName,
icon: <AccountCircleOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Disponibilidad",
icon: <SchedulesIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, [collaborator]);
useEffect(() => {
loadCollaboratorSchedules();
}, [weekDay]);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
updateCollaboratorsSchedules({
companyId: id,
employeeId: cid,
weekDay: weekDay,
schedules: getSchedulesToSave(),
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("Disponibilidad actualizada");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
const loadCollaboratorSchedules = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
findCollaboratorsSchedules({
companyId: id,
employeeId: cid,
weekDay: weekDay,
})
.then((collaborator) => {
if (collaborator === null) {
return;
}
setCollaborator(collaborator);
setSchedulesFromServer(collaborator.schedules);
setFrom(dayjs(new Date()).hour(8).minute(0));
setTo(dayjs(new Date()).hour(12).minute(0));
eventHandler.setEventType(EVENT_TYPES.SLEEP);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const setSchedulesFromServer = (schedules: ScheduleItem[]) => {
setSchedules(
schedules.map((schedule) => {
const dataFrom = schedule.from.split(":");
const fromHoras = parseInt(dataFrom[0]);
const fromMinutos = parseInt(dataFrom[1]);
const dataTo = schedule.to.split(":");
const toHoras = parseInt(dataTo[0]);
const toMinutos = parseInt(dataTo[1]);
return {
from: dayjs(new Date()).hour(fromHoras).minute(fromMinutos),
to: dayjs(new Date()).hour(toHoras).minute(toMinutos),
data: schedule,
};
})
);
};
const getSchedulesToSave = (): ScheduleItem[] => {
return schedules.map((schedule) => {
return {
from: schedule.from.format("HH:mm"),
to: schedule.to.format("HH:mm"),
disabled: schedule.data.disabled || false,
};
});
};
const addButtonHandler = () => {
const newSchedule: SchedulesDateItem = {
from: from.clone(),
to: to.clone(),
data: {
from: from.format("HH:mm"),
to: to.format("HH:mm"),
disabled: false,
},
};
// Función para verificar si el horario se solapa con alguno existente
const hasOverlap = schedules.some((schedule) => {
const existingFrom = dayjs(schedule.from);
const existingTo = dayjs(schedule.to);
return newSchedule.from.isBefore(existingTo) && newSchedule.to.isAfter(existingFrom);
});
if (hasOverlap) {
alert.showError(
"El horario se solapa con uno existente. Por favor, elige otro horario."
);
} else if (newSchedule.from.isAfter(newSchedule.to)) {
alert.showError("La hora de inicio debe ser anterior a la hora de fin.");
} else {
// Agregar el horario si no hay solapamiento
setSchedules([...schedules, newSchedule]);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}
};
const weekDayChangeHandler = (day: number) => {
if (eventHandler.eventType === EVENT_TYPES.NEED_SAVE) {
confirm.show(
"Si no guardas los cambios se perderán. Estas seguro que queres cambiar el dia?",
() => {
setWeekDay(day);
},
() => {
setWeekDay(weekDay);
}
);
} else {
setWeekDay(day);
}
};
const deleteScheduleHandler = (from: string) => {
confirm.show(
"Estas seguro que queres eliminar el horario?",
() => {
const filteredSchedules = schedules.filter(
(schedule) => schedule.data.from != from
);
setSchedules(filteredSchedules);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
},
() => { }
);
};
const checkChangeHandler = (from: string, data: boolean) => {
const updateItem = schedules.find((schedule) => schedule.data.from == from);
if (updateItem) {
updateItem.data.disabled = data;
}
const newSchedules = schedules.map((schedule) => {
if (schedule.data.from == from) {
return {
...schedule,
data: {
...schedule.data,
disabled: data,
},
};
}
return schedule;
});
setSchedules(newSchedules);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
};
return (
<>
<Location />
<div
style={{ display: "flex", flexDirection: "column", marginTop: "20px", gap: "10px" }}
>
<WeekDaySelector value={weekDay} onChange={weekDayChangeHandler} />
<Grid2 container sx={{ marginTop: "0px" }}>
<Grid2 size={3}>
<TextTime
placeholder="Desde"
width="100%"
value={from}
onChange={setFrom}
/>
</Grid2>
<Grid2 size={1}></Grid2>
<Grid2 size={3}>
<TextTime placeholder="Hasta" width="100%" value={to} onChange={setTo} />
</Grid2>
<Grid2 size={1}></Grid2>
<Grid2
size={4}
sx={{ display: "flex", justifyContent: "center", alignItems: "end" }}
>
<Button
text="Agregar"
width="100%"
color="primary"
style={{ height: "42px" }}
onClick={addButtonHandler}
/>
</Grid2>
</Grid2>
{schedules.map((schedule, index) => (
<ScheduleItemRow
key={index}
from={schedule.data.from}
to={schedule.data.to}
disabled={schedule.data.disabled}
onCheckChange={checkChangeHandler}
onDelete={deleteScheduleHandler}
/>
))}
{schedules.length < 1 && (
<MaterialAlert sx={{ width: "100%" }} severity="info">
<strong>No hay horarios creados:</strong> para que los clientes puedan
realizar una reserva con este colaborador en el día seleccionado debe tener
cargada su disponibilidad para cada día de la semana.
</MaterialAlert>
)}
</div>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function CollaboratorServicesLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,174 @@
"use client";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import IconServices from "@mui/icons-material/CategoryOutlined";
import AccountCircleOutlinedIcon from "@mui/icons-material/AccountCircleOutlined";
import { useAlert } from "@store/Alert.Store";
import { useEffect, useState } from "react";
import {
findCollaboratorsServices,
updateCollaboratorsServices,
} from "../../Collaborators.Service";
import { CollaboratorServiceView, ServiceStatusItem } from "@models/Collaborators.model";
import MaterialAlert from "@mui/material/Alert";
import SelectableServiceItem from "@components/SelectebleServiceItem/SelectableServiceItem";
type UrlData = {
id: string;
cid: string;
};
export default function CollaboratorService() {
const data = useParams<UrlData>();
const id = data.id;
const cid = data.cid;
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const alert = useAlert();
const [collaborator, setCollaborator] = useState<CollaboratorServiceView>();
const [serviceStatus, setServiceStatus] = useState<ServiceStatusItem[]>([]);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
if (!collaborator) {
return;
}
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Colaboradores",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators" + "/" + cid,
title: collaborator.fullName,
icon: <AccountCircleOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Servicios activos",
icon: <IconServices sx={{ fontSize: "32px", mr: "10px" }} />,
});
loadServiceStatus();
}, [collaborator]);
useEffect(() => {
loadCollaboratorServices();
}, []);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
updateCollaboratorsServices({
companyId: id,
employeeId: cid,
services: serviceStatus,
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("Servicios actualizados!");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
const loadCollaboratorServices = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
findCollaboratorsServices({
companyId: id,
employeeId: cid,
sessionUser: SessionInfo.userId,
})
.then((collaborator) => {
if (collaborator === null) {
return;
}
setCollaborator(collaborator);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const loadServiceStatus = () => {
if (!collaborator) {
return;
}
const loadServices: ServiceStatusItem[] = [];
for (const service of collaborator.services) {
loadServices.push({
serviceId: service.id,
active: service.active,
});
}
setServiceStatus(loadServices);
};
const updateServiceStatus = (serviceId: string, active: boolean) => {
if (!collaborator) {
return;
}
const loadServices: ServiceStatusItem[] = [];
for (const service of serviceStatus) {
loadServices.push({
serviceId: service.serviceId,
active: service.serviceId === serviceId ? active : service.active,
});
}
setServiceStatus(loadServices);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
};
return (
<>
<Location />
<div
style={{ display: "flex", flexDirection: "column", marginTop: "20px", gap: "10px" }}
>
<MaterialAlert sx={{ width: "100%" }} severity="info">
<strong>Servicios activos:</strong> para agregar o quitar un servicio,
simplemente presione el servicio. Es necesario{" "}
<strong>guardar los cambios</strong> al finalizar.
</MaterialAlert>
{collaborator &&
collaborator.services.map((service, index) => (
<SelectableServiceItem
data={service}
key={"selectableServiceItem." + index}
onChange={(serviceId: string, active: boolean) => {
updateServiceStatus(serviceId, active);
}}
/>
))}
</div>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,8 @@
.collaboratorResultContainer {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: var(--white);
padding: 10px;
}
@@ -0,0 +1,183 @@
"use client";
import { useLocationStore } from "@store/Location.Store";
import GroupsOutlinedIcon from "@mui/icons-material/GroupsOutlined";
import GroupIcon from "@mui/icons-material/Group";
import { useEffect, useState } from "react";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import SearchText from "@components/SearchText/SearchText";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useAlert } from "@store/Alert.Store";
import { CollaboratorView } from "@core/Models/User.model";
import { createCollaborator, findCollaboratorsByEmail } from "../Collaborators.Service";
import Avatar from "@components/Avatar/Avatar";
import Button from "@components/Button/Button";
import MaterialAlert from "@mui/material/Alert";
import style from "./collaborator-add.module.css";
import * as Yup from "yup";
import { useNavigation } from "@hooks/goto";
type UrlData = {
id: string;
};
export default function CollaboratorsAdd() {
const urlData = useParams<UrlData>();
const id = urlData.id;
const alert = useAlert();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const SessionInfo = useSessionStore();
const [collaborator, setCollaborator] = useState<CollaboratorView | null>(null);
const [findEmail, setFindEmail] = useState<string>("");
const [validatedEmail, setValidatedEmail] = useState<boolean>(false);
const { goTo } = useNavigation();
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id,
title: "Organizacion",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/collaborators",
title: "Colaboradores",
icon: <GroupsOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Invitar colaboradores",
icon: <GroupIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
const findCollaborator = async () => {
const schema = Yup.object().shape({
email: Yup.string()
.email("Debe ingresar un e-mail.")
.required("Debe introducir el e-mail."),
});
const validateData = async () => {
try {
await schema.validate({ email: findEmail }, { abortEarly: true });
setValidatedEmail(true);
return true;
} catch (error) {
console.error(error);
setValidatedEmail(false);
return false;
}
};
if (await validateData()) {
eventHandler.setEventType(EVENT_TYPES.LOADING);
findCollaboratorsByEmail({ email: findEmail }).then((data) => {
setCollaborator(data);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
};
const handleCreateCollaborator = () => {
if (!collaborator) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
createCollaborator({
companyId: id,
userId: collaborator.id,
roles: [],
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("Colaborador agregado");
goTo("/admin/org/profile/" + id + "/collaborators");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
return (
<>
<Location />
<div
style={{ display: "flex", flexDirection: "column", marginTop: "20px", gap: "10px" }}
>
<SearchText
value={findEmail}
width="100%"
placeHolder="Buscar por e-mail"
actionText="Buscar"
onChange={(e) => {
setFindEmail(e.target.value.toLowerCase().trim());
}}
onClick={findCollaborator}
/>
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Atención:</b> El colaborador debe estar registrado en TurnosXpress y estar
suscripto como míinimo al plan gratuito.
</MaterialAlert>
{collaborator && (
<>
<div className={style.collaboratorResultContainer}>
<Avatar
size="large"
border="solid"
name={collaborator.fullName}
src={collaborator.avatar}
alt={collaborator.fullName}
/>
<h1>{collaborator.fullName}</h1>
<p>{collaborator.email}</p>
<Button
name="invitarButton"
text="Invitar"
onClick={handleCreateCollaborator}
/>
</div>
</>
)}
{!collaborator && validatedEmail && (
<>
<div className={style.collaboratorResultContainer}>
<h3>No se encontro el colaborador</h3>
<p style={{ textAlign: "center" }}>
Enviá una solicitud a su e-mail, <b>{findEmail}</b> para que se una
a tu organización.
</p>
<Button name="sendInvitation" text="Invitar" onClick={() => {}} />
</div>
</>
)}
{!collaborator && !validatedEmail && (
<>
<div>
<p>
Para agregar un colaborador, ingrese su e-mail. Debe ser el mismo
con el que se registro en la plataforma.
</p>
</div>
</>
)}
</div>
</>
);
}
@@ -0,0 +1,27 @@
"use client";
import { useParams } from "next/navigation";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import Addable from "@components/Addable/Addable";
import { useNavigation } from "@hooks/goto";
type UrlData = {
id: string;
};
export default function OrgLayout({ children }: { children: React.ReactNode }) {
const urlData = useParams<UrlData>();
const id = urlData.id;
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const handleAdd = () => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
goTo("/admin/org/profile/" + id + "/collaborators/add");
};
return (
<>
<Addable onClick={handleAdd}>{children}</Addable>
</>
);
}
@@ -0,0 +1,86 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import { useNavigation } from "@hooks/goto";
import { CompanyEmployeesView } from "@models/Collaborators.model";
import { loadCollaboratorsByCompanyId } from "./Collaborators.Service";
import { useAlert } from "@store/Alert.Store";
import CollaboratorItem from "@core/app/components/CollaboratorItem/CollaboratorItem";
type UrlData = {
id: string;
};
export default function OrganizationProfile() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const alert = useAlert();
const { goTo } = useNavigation();
const [collaborators, setCollaborators] = useState<CompanyEmployeesView[]>([]);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Mis organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Organizacion",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/",
title: "Colaboradores",
icon: null,
});
eventHandler.setEventType(EVENT_TYPES.LOADING);
loadCollaboratorsByCompanyId({ companyId: id })
.then((response) => {
setCollaborators(response);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
}, []);
const handleCollaboratorClick = (collaboratorId: string) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
goTo("/admin/org/profile/" + id + "/collaborators/" + collaboratorId);
};
return (
<>
<Location />
<div
style={{ display: "flex", flexDirection: "column", marginTop: "20px", gap: "10px" }}
>
{collaborators.map((collaborator) => (
<CollaboratorItem
key={"collItem." + collaborator.id}
data={collaborator}
onClick={() => handleCollaboratorClick(collaborator.id)}
/>
))}
</div>
</>
);
}
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function OrgConfigLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,201 @@
"use client";
import { useParams } from "next/navigation";
import { useState, useEffect } from "react";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useAlert } from "@store/Alert.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import OrgConfigIcon from "@mui/icons-material/SettingsOutlined";
import { loadOrganizationsById, updateOrganizationsById, uploadHeaderImage } from "../Org.Service";
import { MyOranizationsView, UpdateCompanyParams } from "@models/Company.model";
import { PopoverPicker } from "@core/app/components/ColorPicker/ColorPicker";
import { useOrganizationHeaderStore } from "@core/Store/OrganizationHeader.Store";
import { Grid2 } from "@mui/material";
import ImagePicker from "@core/app/components/ImagePicker/ImagePicker";
type UrlData = {
id: string;
};
export default function OrganizationEdit() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const organizationHeader = useOrganizationHeaderStore();
const alert = useAlert();
const [headerImage, setHeaderImage] = useState("");
const [headerColor, setHeaderColor] = useState("");
const [headerFontColor, setHeaderFontColor] = useState("");
const [headerFontShadowColor, setHeaderFontShadowColor] = useState("");
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Organizacion",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Perfil",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Perzonalización",
icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
eventHandler.setEventType(EVENT_TYPES.LOADING);
loadOrganizationsById({ id: id, sessionUser: SessionInfo.userId })
.then((response: MyOranizationsView) => {
setHeaderImage(response.headerFile);
setHeaderColor(response.headerColor);
setHeaderFontColor(response.headerFontColor);
setHeaderFontShadowColor(response.headerFontShadowColor);
organizationHeader.setHeaderColor(response.headerColor);
organizationHeader.setHeaderFontColor(response.headerFontColor);
organizationHeader.setHeaderFontShadowColor(response.headerFontShadowColor);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}, []);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const updData: UpdateCompanyParams = {
id: id,
headerColor: headerColor,
headerFontColor: headerFontColor,
headerFontShadowColor: headerFontShadowColor,
sessionUser: SessionInfo.userId,
};
updateOrganizationsById(updData)
.then(() => {
alert.showSuccess("Color del encabezado actualizado");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
const handleHeaderImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const file = e.target.files?.[0];
if (!file) {
return;
}
const formData = new FormData();
formData.append("companyId", id);
formData.append("sessionUser", SessionInfo.userId); // Añade el ID del usuario
formData.append("file", file);
uploadHeaderImage(formData)
.then((res) => {
if (res.success) {
alert.showSuccess("Header actualizado");
}
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
return (
<>
<Location />
<div style={{ marginTop: "20px", color: "var(--black)", fontWeight: "bold" }}>
Colores
</div>
<Grid2 container>
<Grid2>
<PopoverPicker
placeholder="Fondo"
color={headerColor}
onChange={(color: string) => {
setHeaderColor(color);
organizationHeader.setHeaderColor(color);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
<Grid2 sx={{ width: "40px" }}></Grid2>
<Grid2>
<PopoverPicker
placeholder="Fuente"
color={headerFontColor}
onChange={(color: string) => {
setHeaderFontColor(color);
organizationHeader.setHeaderFontColor(color);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
<Grid2 sx={{ width: "40px" }}></Grid2>
<Grid2>
<PopoverPicker
placeholder="Sombra"
color={headerFontShadowColor}
onChange={(color: string) => {
setHeaderFontShadowColor(color);
organizationHeader.setHeaderFontShadowColor(color);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
</Grid2>
<div
style={{
marginTop: "20px",
color: "var(--black)",
fontWeight: "bold",
paddingTop: "5px",
paddingBottom: "10px",
}}
>
Imagen
</div>
<ImagePicker
src={headerImage}
alt="Logo"
style={{ width: "100%", height: "150px" }}
readOnly={false}
onChange={handleHeaderImageChange}
imageSizeSrc="/org-header-size.webp"
mbLimit={5}
/>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,50 @@
import ApiRequest from "@services/Api.Service";
import * as Yup from "yup";
import { ApiError } from "@core/Models/Server.Error.model";
import {
CreateDiscountEmailParams,
DeleteDiscountEmailParams,
IDiscountEmail,
PaginateDiscountsEmailParams,
PaginateDiscountsEmailViewResults,
} from "@core/Models/DiscountsEmail.model";
const schemaDiscountEmailAdd = Yup.object().shape({
email: Yup.string().email("Debe introducir un email válido").required("Debe introducir el email."),
discountId: Yup.string().required("Debe introducir el código del descuento."),
startDate: Yup.date().required("Debe introducir la fecha de inicio."),
endDate: Yup.date().required("Debe introducir la fecha de fin."),
sessionUser: Yup.string().required("Para crear un descuento debe iniciar sesión."),
});
export const createDiscountEmail = async (data: CreateDiscountEmailParams): Promise<IDiscountEmail> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaDiscountEmailAdd.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new ApiError(400, yupErrors.message);
}
return ApiRequest.post<IDiscountEmail>("discounts-email/create", data);
};
export const findDiscountsEmailByOrganization = async (
data: PaginateDiscountsEmailParams
): Promise<PaginateDiscountsEmailViewResults> => {
return ApiRequest.post<PaginateDiscountsEmailViewResults>("discounts-email/paginate", data);
};
export const deleteDiscountEmail = async (data: DeleteDiscountEmailParams): Promise<void> => {
return ApiRequest.post<void>("discounts-email/delete", data);
};
@@ -0,0 +1,28 @@
"use client";
import { useParams } from "next/navigation";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import { useNavigation } from "@hooks/goto";
import Addable from "@components/Addable/Addable";
type UrlData = {
id: string;
};
export default function ClientLayout({ children }: { children: React.ReactNode }) {
const urlData = useParams<UrlData>();
const id = urlData.id;
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const handleAdd = () => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
goTo("/admin/org/profile/" + id + "/discounts-email/new");
};
return (
<>
<Addable onClick={handleAdd}>{children}</Addable>
</>
);
}
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function CreateDiscountsLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,173 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import DiscountsIcon from "@mui/icons-material/LocalOfferOutlined";
import Textbox from "@components/Textbox/Textbox";
import TextDate from "@core/app/components/TextDate/TextDate";
import TextObjectFilter from "@components/TextObjectFilter/TextObjectFilter";
import filterObjects from "@core/app/components/TextObjectFilter/Filter.Service";
import TextObjectFilterItem from "@components/TextObjectFilterItem/TextObjectFilterItem";
import { FindDiscountsParams } from "@core/Models/Discounts.model";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import dayjs, { Dayjs } from "dayjs";
import "dayjs/locale/es";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import { createDiscountEmail } from "../DiscountsEmail.Service";
dayjs.locale("es");
type UrlData = {
id: string;
};
export default function DiscountCreate() {
const data = useParams<UrlData>();
const id = data.id;
const alert = useAlert();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const [email, setEmail] = useState("");
const [dateStart, setDateStart] = useState<Dayjs>(dayjs());
const [dateEnd, setDateEnd] = useState<Dayjs>(dayjs());
const { goTo } = useNavigation();
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id,
title: "Origanizacion",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/discounts",
title: "Descuentos",
icon: <DiscountsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/create",
title: "Crear",
icon: <DiscountsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
const [discountFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Descuento:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre del descuento para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [discountFilterData, setDiscountFilterData] = useState<TextObjectFilterResult[]>([]);
const [discountId, setDiscountId] = useState<string>("");
useEffect(() => {
if (!SessionInfo.userId) return;
const filter: FindDiscountsParams = {
companyId: id,
sessionUser: SessionInfo.userId,
};
filterObjects<FindDiscountsParams>("discounts/filter-all", filter).then((res) => {
setDiscountFilterData(res);
});
}, [SessionInfo.userId]);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
createDiscountEmail({
companyId: id,
email: email,
discountId: discountId,
startDate: dateStart.toDate(),
endDate: dateEnd.toDate(),
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess(`El descuento ha sido creado`);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
setTimeout(() => {
goTo("/admin/org/profile/" + id + "/discounts-email");
}, 2000);
})
.catch((error) => {
alert.showError(error.format());
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
});
}
}, [eventHandler.eventType]);
return (
<>
<Location />
<Textbox
placeholder="Email"
type="text"
value={email}
width="100%"
onChange={(e) => {
setEmail(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<TextObjectFilter
placeholder="Descuento"
content={discountFilterContent}
data={discountFilterData}
onChange={(id) => {
setDiscountId(id);
}}
selectedId={discountId}
/>
<div>
<TextDate
placeholder="Desde"
selectedDate={dateStart}
onChange={(value: dayjs.Dayjs) => {
setDateStart(value);
}}
/>
</div>
<div>
<TextDate
placeholder="Hasta"
selectedDate={dateEnd}
onChange={(value: dayjs.Dayjs) => {
setDateEnd(value);
}}
/>
</div>
<div style={{ marginTop: "15px" }}></div>
</>
);
}
@@ -0,0 +1,380 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import DiscountsIcon from "@mui/icons-material/LocalOfferOutlined";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import DataGrid, { DataGridColumn, DataGridRow } from "@components/DataGrid/DataGrid";
import { useSessionStore } from "@store/Sesion.Store";
import { formatPrice } from "@core/app/helpers/Numbers";
import {
DiscountEmailView,
PaginateDiscountsEmailParams,
PaginateDiscountsEmailViewResults,
} from "@core/Models/DiscountsEmail.model";
import { formatDiscountType } from "../discounts/Discounts.Service";
import { DiscountType, FindDiscountsParams } from "@core/Models/Discounts.model";
import { deleteDiscountEmail, findDiscountsEmailByOrganization } from "./DiscountsEmail.Service";
import { ThemeProvider } from "@emotion/react";
import turnosXpressTheme from "@core/app/theme/turnosXpress";
import { ButtonGroup } from "@mui/material";
import ButtonMaterial from "@mui/material/Button";
import { useNavigation } from "@core/app/hooks/goto";
import dayjs from "dayjs";
import IconButton from "@mui/material/IconButton";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { useAlert } from "@core/Store/Alert.Store";
import Textbox from "@components/Textbox/Textbox";
import Filterable from "@core/app/components/Filterable/Filterable";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import { useFilterStore } from "@core/Store/Filter.Store";
import filterObjects from "@components/TextObjectFilter/Filter.Service";
type UrlData = {
id: string;
};
//----------------->
interface FilterContentProps {
discountData: TextObjectFilterResult[];
setDiscountId: (id: string) => void;
filterEmail: string;
setFilterEmail: (email: string) => void;
}
const FilterContent = (props: FilterContentProps) => {
const [auxEmailFilter, setAuxEmailFilter] = useState<string>("");
const [discountFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Descuento:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el código del descuento para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
return (
<>
<Textbox
placeholder="Email"
type="text"
value={auxEmailFilter || ""}
width="100%"
onChange={(e) => {
props.setFilterEmail(e.target.value);
setAuxEmailFilter(e.target.value);
}}
/>
<TextObjectFilter
placeholder="Descuento"
content={discountFilterContent}
data={props.discountData}
onChange={(id) => {
props.setDiscountId(id);
}}
/>
</>
);
};
//-----------------<
export type DiscountEmailRow = DataGridRow & DiscountEmailView;
export default function DiscountsEmailGrid() {
const data = useParams<UrlData>();
const oid = data.id;
const location = useLocationStore();
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const alert = useAlert();
const confirm = useConfirmStore();
const filter = useFilterStore();
const [searchByFilters, setSearchByFilters] = useState<boolean>(false);
const [filterEmail, setFilterEmail] = useState<string>("");
//--------->
const [discountFilterData, setDiscountFilterData] = useState<TextObjectFilterResult[]>([]);
const [discountId, setDiscountId] = useState<string>("");
useEffect(() => {
if (!SessionInfo.userId) {
return;
}
const filter: FindDiscountsParams = {
companyId: oid,
sessionUser: SessionInfo.userId,
};
filterObjects<FindDiscountsParams>("discounts/filter-all", filter).then((res) => {
setDiscountFilterData(res);
});
}, [SessionInfo.userId]);
//---------<
const { goTo } = useNavigation();
const { userId } = useSessionStore();
const [dataGridColumns] = useState<DataGridColumn<DiscountEmailRow>[]>([
{
field: "email",
title: "Email",
width: null,
render: (value: DiscountEmailRow) => (
<div style={{ paddingBottom: "10px" }}>
<div>
<div style={{ paddingLeft: "10px", paddingTop: "10px", fontWeight: "bold" }}>
{value.discountName + " (" + value.discountCode + ")"}
</div>
<div style={{ paddingLeft: "10px", paddingTop: "0px", fontWeight: "bold" }}>
{value.email}
</div>
<div
style={{
paddingLeft: "10px",
paddingTop: "0px",
fontWeight: "normal",
color: "var(--wine-darkest)",
}}
>
{dayjs(value.startDate).format("DD/MM/YYYY")} -{" "}
{dayjs(value.endDate).format("DD/MM/YYYY")}
</div>
<div style={{ paddingLeft: "10px", color: "var(--gray-dark)" }}>
{formatDiscountType(value.discountType)}
<span
style={{
paddingLeft: "10px",
color: "var(--green-darkestX2)",
}}
>
{value.discountType === DiscountType.FIXED
? formatPrice(value.discountValue)
: value.discountValue + "%"}
</span>
</div>
</div>
</div>
),
},
{
field: "actions",
title: "",
width: 50,
render: (value: DiscountEmailRow) => (
<div
style={{
paddingBottom: "10px",
height: "100%",
}}
>
<IconButton onClick={() => handleDelete(value)}>
<DeleteIcon sx={{ mr: "15px" }} />
</IconButton>
</div>
),
},
]);
const [pageSize] = useState<number>(
parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE ? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE : "10"
)
);
const [discounts, setDiscounts] = useState<PaginateDiscountsEmailViewResults>();
const discountsRef = useRef(discounts);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Mis organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + oid,
title: "Organizacion",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Descuentos por email",
icon: <DiscountsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
loadDiscountsEmail(1, pageSize);
}, []);
useEffect(() => {
discountsRef.current = discounts;
}, [discounts]);
useEffect(() => {
if (!searchByFilters) {
return;
}
let loadPage = 1;
if (discountsRef.current) {
loadPage = discountsRef.current.page;
}
loadDiscountsEmail(loadPage, pageSize);
}, [discountId, filterEmail, searchByFilters]);
useEffect(() => {
filter.clear();
filter.setTitle("Filtrar Descuentos");
filter.clearFilterData();
filter.setContent(
<FilterContent
discountData={discountFilterData}
setDiscountId={setDiscountId}
filterEmail={filterEmail}
setFilterEmail={setFilterEmail}
/>
);
filter.setOnFilter(() => {
setSearchByFilters(true);
});
return () => {
filter.clear();
};
}, [discountFilterData]);
const loadDiscountsEmail = (page: number = 1, pageSize: number = 30) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const filterData: PaginateDiscountsEmailParams = {
companyId: oid,
page: page,
limit: pageSize,
sessionUser: userId,
};
if (discountId) {
filterData.discountId = discountId;
}
if (filterEmail !== "") {
filterData.email = filterEmail;
}
findDiscountsEmailByOrganization(filterData)
.then((res) => {
setDiscounts(res);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
setSearchByFilters(false);
});
};
const handleDelete = (discount: DiscountEmailRow) => {
if (!discount.id) {
return;
}
confirm.show(
`¿Estás seguro de eliminar el descuento ${discount.discountName} para ${discount.email}?`,
() => {
deleteDiscountEmail({
id: discount.id,
companyId: oid,
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("Descuento eliminado correctamente");
loadDiscountsEmail();
})
.catch((err) => {
alert.showError(err.format());
});
},
() => {}
);
};
return (
<>
<Location />
<Filterable />
<div style={{ display: "flex", justifyContent: "center", marginTop: "15px" }}>
<ThemeProvider theme={turnosXpressTheme}>
<ButtonGroup variant="outlined" aria-label="Basic button group">
<ButtonMaterial
variant="outlined"
onClick={() => {
goTo(`/admin/org/profile/${oid}/discounts`);
}}
>
Descuentos
</ButtonMaterial>
<ButtonMaterial variant="contained">Asignados</ButtonMaterial>
</ButtonGroup>
</ThemeProvider>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: "60px",
position: "absolute",
left: "0px",
right: "0px",
top: "60px",
bottom: "10px",
}}
>
<DataGrid
columns={dataGridColumns}
data={discounts ? discounts.data : []}
pagination={true}
page={discounts?.page}
pages={discounts?.pages}
pageSize={pageSize}
handleRefresh={loadDiscountsEmail}
/>
</div>
</>
);
}
@@ -0,0 +1,64 @@
import ApiRequest from "@services/Api.Service";
import {
IDiscount,
PaginateDiscountsResults,
CreateDiscountParams,
PaginateDiscountsParams,
DeleteDiscountParams,
DiscountType,
} from "@models/Discounts.model";
import * as Yup from "yup";
import { ApiError } from "@core/Models/Server.Error.model";
const schemaDiscountAdd = Yup.object().shape({
name: Yup.string().required("Debe introducir el nombre del descuento."),
code: Yup.string().required("Debe introducir el código del descuento."),
type: Yup.string().required("Debe seleccionar un tipo de descuento."),
value: Yup.number()
.required("Debe introducir un valor para el descuento.")
.typeError("El valor debe ser un número."),
sessionUser: Yup.string().required("Para crear un descuento debe iniciar sesión."),
});
export const createDiscount = async (data: CreateDiscountParams): Promise<IDiscount> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaDiscountAdd.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new ApiError(400, yupErrors.message);
}
return ApiRequest.post<IDiscount>("discounts/create", data);
};
export const findDiscountsByOrganization = async (
data: PaginateDiscountsParams
): Promise<PaginateDiscountsResults> => {
return ApiRequest.post<PaginateDiscountsResults>("discounts/paginate", data);
};
export const deleteDiscount = async (data: DeleteDiscountParams): Promise<void> => {
return ApiRequest.post<void>("discounts/delete", data);
};
export const formatDiscountType = (type: string) => {
switch (type) {
case DiscountType.PERCENTAGE:
return "Porcentaje";
case DiscountType.FIXED:
return "Monto";
}
return "n/a";
};
@@ -0,0 +1,28 @@
"use client";
import { useParams } from "next/navigation";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import { useNavigation } from "@hooks/goto";
import Addable from "@components/Addable/Addable";
type UrlData = {
id: string;
};
export default function ClientLayout({ children }: { children: React.ReactNode }) {
const urlData = useParams<UrlData>();
const id = urlData.id;
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const handleAdd = () => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
goTo("/admin/org/profile/" + id + "/discounts/new");
};
return (
<>
<Addable onClick={handleAdd}>{children}</Addable>
</>
);
}
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function CreateDiscountsLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,134 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import { useSessionStore } from "@store/Sesion.Store";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import DiscountsIcon from "@mui/icons-material/LocalOfferOutlined";
import Textbox from "@components/Textbox/Textbox";
import { useAlert } from "@store/Alert.Store";
import { useNavigation } from "@hooks/goto";
import { createDiscount } from "../Discounts.Service";
import { DiscountType } from "@models/Discounts.model";
import TextSelect from "@components/TextSelect/TextSelect";
type UrlData = {
id: string;
};
export default function DiscountCreate() {
const data = useParams<UrlData>();
const id = data.id;
const alert = useAlert();
const SessionInfo = useSessionStore();
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const [name, setName] = useState("");
const [code, setCode] = useState("");
const [type, setType] = useState<DiscountType>(DiscountType.PERCENTAGE);
const [value, setValue] = useState(0);
const { goTo } = useNavigation();
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id,
title: "Origanizacion",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/discounts",
title: "Descuentos",
icon: <DiscountsIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/create",
title: "Crear",
icon: <DiscountsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
createDiscount({
companyId: id,
name: name,
code: code,
type: type,
value: value,
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess(`El descuento ha sido creado`);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
setTimeout(() => {
goTo("/admin/org/profile/" + id + "/discounts");
}, 2000);
})
.catch((error) => {
alert.showError(error.format());
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
});
}
}, [eventHandler.eventType]);
return (
<>
<Location />
<Textbox
placeholder="Nombre"
type="text"
value={name}
width="100%"
onChange={(e) => {
setName(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
placeholder="Código"
type="text"
value={code}
width="100%"
onChange={(e) => {
setCode(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<TextSelect
label="Tipo"
selectedValue={type}
data={[
{ id: DiscountType.FIXED, text: "Fijo" },
{ id: DiscountType.PERCENTAGE, text: "Porcentaje" },
]}
setSelectedValue={(value) => {
setType(value as DiscountType);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
style={{ width: "100%", marginTop: "15px" }}
/>
<Textbox
placeholder="Valor"
type="number"
value={value.toString()}
width="100%"
onChange={(e) => {
setValue(Number(e.target.value));
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<div style={{ marginTop: "15px" }}></div>
</>
);
}
@@ -0,0 +1,179 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import DiscountsIcon from "@mui/icons-material/LocalOfferOutlined";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import DataGrid, { DataGridColumn, DataGridRow } from "@components/DataGrid/DataGrid";
import { findDiscountsByOrganization, formatDiscountType } from "./Discounts.Service";
import {
IDiscount,
PaginateDiscountsResults,
PaginateDiscountsParams,
DiscountType,
} from "@models/Discounts.model";
import { useSessionStore } from "@store/Sesion.Store";
import { formatPrice } from "@core/app/helpers/Numbers";
import { ThemeProvider } from "@emotion/react";
import turnosXpressTheme from "@core/app/theme/turnosXpress";
import { ButtonGroup } from "@mui/material";
import ButtonMaterial from "@mui/material/Button";
import { useNavigation } from "@core/app/hooks/goto";
type UrlData = {
id: string;
};
export type DiscountRow = DataGridRow & IDiscount;
export default function DiscountsGrid() {
const data = useParams<UrlData>();
const oid = data.id;
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const { userId } = useSessionStore();
const [dataGridColumns] = useState<DataGridColumn<DiscountRow>[]>([
{
field: "name",
title: "Nombre",
width: null,
render: (value: DiscountRow) => (
<div>
<div style={{ paddingLeft: "10px", paddingTop: "10px", fontWeight: "bold" }}>
{value.name + " (" + value.code + ")"}
</div>
<div style={{ paddingLeft: "10px", color: "var(--gray-dark)" }}>
{formatDiscountType(value.type)}
</div>
</div>
),
},
{
field: "value",
title: "Valor",
width: 150,
render: (value: DiscountRow) => (
<div>
<div
style={{
paddingTop: "10px",
textAlign: "right",
paddingRight: "10px",
color: "var(--green-darkestX2)",
}}
>
{value.type === DiscountType.FIXED ? formatPrice(value.value) : value.value + "%"}
</div>
</div>
),
},
]);
const [pageSize] = useState<number>(
parseInt(
process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE ? process.env.NEXT_PUBLIC_DATAGRID_PAGE_SIZE : "10"
)
);
const [discounts, setDiscounts] = useState<PaginateDiscountsResults>();
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Mis organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + oid,
title: "Organizacion",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Descuentos",
icon: <DiscountsIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
loadDiscounts(1, pageSize);
}, []);
const loadDiscounts = (page: number = 1, pageSize: number = 30) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const filterData: PaginateDiscountsParams = {
companyId: oid,
page: page,
limit: pageSize,
sessionUser: userId,
};
findDiscountsByOrganization(filterData)
.then((res) => {
setDiscounts(res);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
};
const handleDiscountClick = (discount: DiscountRow) => {
// goTo("/admin/org/" + oid + "/discount/" + discount.id + "/profile");
console.log("Discount clicked:", discount);
};
return (
<>
<Location />
<div style={{ display: "flex", justifyContent: "center", marginTop: "15px" }}>
<ThemeProvider theme={turnosXpressTheme}>
<ButtonGroup variant="outlined" aria-label="Basic button group">
<ButtonMaterial variant="contained">Descuentos</ButtonMaterial>
<ButtonMaterial
variant="outlined"
onClick={() => {
goTo(`/admin/org/profile/${oid}/discounts-email`);
}}
>
Asignados
</ButtonMaterial>
</ButtonGroup>
</ThemeProvider>
</div>
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: "60px",
position: "absolute",
left: "0px",
right: "0px",
top: "60px",
bottom: "10px",
}}
>
<DataGrid
columns={dataGridColumns}
data={discounts ? discounts.data : []}
pagination={true}
page={discounts?.page}
pages={discounts?.pages}
pageSize={pageSize}
handleRefresh={loadDiscounts}
handleRowClick={handleDiscountClick}
/>
</div>
</>
);
}
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function OrgConfigLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,609 @@
"use client";
import { useParams } from "next/navigation";
import { useState, useEffect } from "react";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useAlert } from "@store/Alert.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import OrgConfigIcon from "@mui/icons-material/SettingsOutlined";
import Textbox from "@components/Textbox/Textbox";
import TextPhone from "@components/TextPhone/TextPhone";
import MaterialAlert from "@mui/material/Alert";
import * as Yup from "yup";
import {
loadOrganizationsById,
savePublishedStatus,
updateOrganizationsById,
} from "../Org.Service";
import {
COMPANY_PUBLISHED_STATUS,
MyOranizationsView,
SetOrganizationPublishedStatusParams,
UpdateCompanyParams,
} from "@models/Company.model";
import { ApiError } from "@models/Server.Error.model";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import categories from "@core/Models/Categories.type";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import { getSlug } from "@core/app/helpers/Slug";
import { validatePhone } from "@core/helpers/validatePhone";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import Switch from "@core/app/components/Switch/Switch";
import { FindTemplatesParams } from "@core/Models/Templates.model";
import filterObjects from "@core/app/components/TextObjectFilter/Filter.Service";
type UrlData = {
id: string;
};
export default function OrganizationEdit() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
const confirm = useConfirmStore();
const [orgName, setOrgName] = useState("");
const [orgDescription, setOrgDescription] = useState("");
const [areaCode, setAreaCode] = useState("");
const [phone, setPhone] = useState("");
const [heatMapFraction, setHeatMapFraction] = useState("");
const [appointmentTime, setAppointmentTime] = useState("");
const [cancellationTime, setCancellationTime] = useState("");
const [appointmentAlert, setAppointmentAlert] = useState("");
const [orgSlug, setOrgSlug] = useState("");
const [orgPublished, setOrgPublished] = useState<COMPANY_PUBLISHED_STATUS | undefined>(
undefined
);
const [savePublishedStatusAvailable, setSavePublishedStatusAvailable] = useState(false);
const [combosLoaded, setCombosLoaded] = useState<boolean>(false);
const [categoryFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Categoría:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el nombre de la categoría para buscarla.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [categoryFilterData, setCategoryFilterData] = useState<TextObjectFilterResult[]>([]);
const [categoryId, setCategoryId] = useState<string>("");
/* WAP NOTIF */
const [templateWapNotifFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Plantilla:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre de la plantilla para buscarla.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [templateWapNotifFilterData, setTemplateWapNotifFilterData] = useState<
TextObjectFilterResult[]
>([]);
const [templateWapNotifId, setTemplateWapNotifId] = useState<string>("");
useEffect(() => {
if (!SessionInfo.userId) return;
loadTemplatesCombos();
}, [SessionInfo.userId]);
/* WAP ALTA */
const [templateWapAltaFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>
Seleccionar Plantilla:
</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>
Ingrese el nombre de la plantilla para buscarla.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [templateWapAltaFilterData, setTemplateWapAltaFilterData] = useState<
TextObjectFilterResult[]
>([]);
const [templateWapAltaId, setTemplateWapAltaId] = useState<string>("");
const loadTemplatesCombos = () => {
if (!SessionInfo.userId) return;
const filter: FindTemplatesParams = {
companyId: id,
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
filterObjects<FindTemplatesParams>("templates/filter-all", filter).then((res) => {
setTemplateWapNotifFilterData(res);
const filter: FindTemplatesParams = {
companyId: id,
sessionUser: SessionInfo.userId,
};
filterObjects<FindTemplatesParams>("templates/filter-all", filter).then((res) => {
setTemplateWapAltaFilterData(res);
setCombosLoaded(true);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
});
};
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Perfil",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Configuración",
icon: <OrgConfigIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
loadCategories();
}, []);
useEffect(() => {
if (combosLoaded) {
eventHandler.setEventType(EVENT_TYPES.LOADING);
loadOrganizationsById({ id: id, sessionUser: SessionInfo.userId })
.then((response: MyOranizationsView) => {
setOrgName(response.name);
setOrgDescription(response.description);
setHeatMapFraction(response.heatMapFraction.toString());
setAppointmentTime(response.appointmentTime.toString());
setCancellationTime(response.cancellationTime.toString());
setAppointmentAlert(response.appointmentAlert);
setAreaCode(response.phoneAreaCode);
setPhone(response.phoneNumber);
setCategoryId(response.categoryId.toString());
setOrgSlug(getSlug(response.name));
setOrgPublished(response.published);
requestAnimationFrame(() => {
setTemplateWapNotifId(response.templateWapNotifId);
setTemplateWapAltaId(response.templateWapAltaId);
});
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [combosLoaded]);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const validationPhoneResult = validatePhone(areaCode, phone);
if (!validationPhoneResult.isValid) {
confirm.show(
validationPhoneResult.message,
() => {},
() => {}
);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
return;
}
const data: UpdateCompanyParams = {
id: id,
name: orgName,
description: orgDescription,
categoryId: Number(categoryId),
sessionUser: SessionInfo.userId,
heatMapFraction: heatMapFraction === "" ? undefined : parseInt(heatMapFraction),
appointmentTime: appointmentTime === "" ? undefined : parseInt(appointmentTime),
cancellationTime: cancellationTime === "" ? undefined : parseInt(cancellationTime),
appointmentAlert: appointmentAlert,
phoneAreaCode: areaCode,
phoneNumber: phone,
templateWapNotifId: templateWapNotifId === "" ? undefined : templateWapNotifId,
templateWapAltaId: templateWapAltaId === "" ? undefined : templateWapAltaId,
};
const schema = Yup.object().shape({
id: Yup.string().required("En este momento no es posible procesar su solicitud."),
name: Yup.string().required("Debe introducir el nombre de la organización."),
description: Yup.string().required(
"Debe introducir la descripción de la organización."
),
categoryId: Yup.number().required(
"Debe seleccionar una categoría para la organización."
),
phoneAreaCode: Yup.number()
.typeError("Código de área debe ser un número.")
.transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value
)
.nullable(),
phoneNumber: Yup.number()
.typeError("El teléfono debe ser un número.")
.transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value
)
.nullable(),
appointmentAlert: Yup.string().optional().default(""),
sessionUser: Yup.string().required(
"Para crear una organización debe iniciar sesión."
),
heatMapFraction: Yup.number()
.typeError("Fracción de tiempo debe ser un número.")
.transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value
)
.nullable()
.integer("Fracción de tiempo debe ser un número entero en minutos.")
.positive("Fracción de tiempo debe ser un número."),
appointmentTime: Yup.number()
.typeError("El Plazo Mínimo de Reserva debe ser un número.")
.transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value
)
.nullable()
.integer("El Plazo Mínimo de Reserva debe ser un número entero en horas.")
.positive("El Plazo Mínimo de Reserva debe ser un número positivo."),
cancellationTime: Yup.number()
.typeError("El Plazo Mínimo de Cancelación debe ser un número.")
.transform((value, originalValue) =>
originalValue.toString().trim() === "" ? null : value
)
.nullable()
.integer("El Plazo Mínimo de Cancelación debe ser un número entero en horas.")
.positive("El Plazo Mínimo de Cancelación debe ser un número positivo."),
templateWapNotifId: Yup.string().optional().default(""),
templateWapAltaId: Yup.string().optional().default(""),
templateEmailNotifId: Yup.string().optional().default(""),
templateEmailAltaId: Yup.string().optional().default(""),
});
schema
.validate(data, { abortEarly: true })
.then(() => {
updateOrganizationsById(data)
.then(() => {
alert.showSuccess("Organizacion actualizada!");
})
.catch((error: ApiError) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
})
.catch((error: Yup.ValidationError) => {
alert.showError(error.message);
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}, [eventHandler.eventType]);
const loadCategories = () => {
setCategoryFilterData(
categories.map((category) => ({
id: category.id.toString(),
name: category.name,
description: category.description,
}))
);
};
const getPublishedStatusText = () => {
if (orgPublished === COMPANY_PUBLISHED_STATUS.PUBLISHED) {
return "Público";
} else {
return "Privado";
}
};
const savePublishedStatusHandler = () => {
if (!orgPublished) {
return;
}
const data: SetOrganizationPublishedStatusParams = {
companyId: id,
published: orgPublished,
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
savePublishedStatus(data)
.then(() => {
alert.showSuccess("Estado de publicación actualizado");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
useEffect(() => {
if (!SessionInfo.userId) {
return;
}
if (!id) {
return;
}
if (!savePublishedStatusAvailable) {
return;
}
savePublishedStatusHandler();
}, [orgPublished]);
return (
<>
<Location />
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<p style={{ marginTop: "10px" }}>
El perfil de la organización no será visible para los clientes hasta que se
publique.
</p>
<div
style={{
marginTop: "10px",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Switch
checked={orgPublished === COMPANY_PUBLISHED_STATUS.PUBLISHED}
onChange={(checked) => {
setSavePublishedStatusAvailable(true);
setOrgPublished(
checked
? COMPANY_PUBLISHED_STATUS.PUBLISHED
: COMPANY_PUBLISHED_STATUS.PRIVATE
);
}}
/>
<span
style={
orgPublished === COMPANY_PUBLISHED_STATUS.PUBLISHED
? { color: "var(--green-darkestX2)" }
: { color: "var(--error)" }
}
>
{getPublishedStatusText()}
</span>
</div>
</div>
<Textbox
placeholder="Nombre de la organización"
type="text"
value={orgName}
width="100%"
onChange={(e) => {
setOrgName(e.target.value);
setOrgSlug(getSlug(e.target.value));
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<a
style={{
fontSize: "14px",
paddingLeft: "10px",
fontWeight: "600",
}}
href={"https://turnosxpress.com.ar/" + orgSlug}
>
https://turnosxpress.com.ar/{orgSlug}
</a>
<Textbox
placeholder="Descripción de la organización"
type="text"
value={orgDescription}
width="100%"
onChange={(e) => {
setOrgDescription(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<TextObjectFilter
placeholder="Categoría"
content={categoryFilterContent}
data={categoryFilterData}
selectedId={categoryId}
onChange={(id) => {
setCategoryId(id);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<TextPhone
label="Teléfono"
areaCode={areaCode}
number={phone}
width="100%"
onChangeArea={(e) => {
setAreaCode(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
onChangePhone={(e) => {
setPhone(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Alerta para Turnos:</b> El mensaje que establezca aquí se mostrará al cliente al
momento de realizar una reserva.
</MaterialAlert>
<Textbox
name="appointmentAlert"
placeholder="Alerta para turnos"
type="comments"
limitSize={255}
value={appointmentAlert}
width="100%"
onChange={(e) => {
setAppointmentAlert(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
style={{ minHeight: "120px" }}
/>
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Fracción de tiempo:</b> Este parámetro (en minutos) establece la duración de un
turno. Por ejemplo, si lo configuras en 15, los clientes podrán reservar a las
08:00, 08:15, 08:30, etc. Si lo configuras en 30, podrán reservar a las 08:00,
08:30, 09:00, etc. Para turnos de una hora, puedes poner 60.
</MaterialAlert>
<Textbox
name="heatMapFraction"
placeholder="Fracción de tiempo"
type="text"
value={heatMapFraction}
width="100%"
onChange={(e) => {
setHeatMapFraction(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Plazo mínimo de Reserva:</b> Este parámetro (en horas) indica el plazo de tiempo
mínimo en el que es posible para el cliente realizar una reserva. Por ejemplo, si lo
configuras en 2, y un cliente intenta reservar a las 10:00, no podrá hacerlo si son
las 09:00 o más tarde. Si lo configuras en 24, el cliente deberá reservar con al
menos 24 horas de anticipación.
</MaterialAlert>
<Textbox
name="appointmentTime"
placeholder="Plazo Mínimo de Reserva"
type="text"
value={appointmentTime}
width="100%"
onChange={(e) => {
setAppointmentTime(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Plazo mínimo de Cancelación:</b> Este parámetro (en horas) indica el plazo de
tiempo mínimo en el que es posible para el cliente cancelar una reserva. Por
ejemplo, si lo configuras en 2, y un cliente intenta cancelar una reserva para las
10:00, no podrá hacerlo si son las 09:00 o más tarde. Si lo configuras en 24, el
cliente deberá cancelar con al menos 24 horas de anticipación.
</MaterialAlert>
<Textbox
name="cancellationTime"
placeholder="Plazo Mínimo de Cancelación"
type="text"
value={cancellationTime}
width="100%"
onChange={(e) => {
setCancellationTime(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Notificaciones:</b> Estos parametros registran las plantillas a utilizar al
enviar recordatorios por Whatsapp y por Email. Para crear las plantillas debe ir a
la sección de Plantillas en el panel de control. Si no se configura ninguna
plantilla se utilizará la plantilla por defecto.
</MaterialAlert>
<TextObjectFilter
placeholder="Recordatorios"
content={templateWapNotifFilterContent}
data={templateWapNotifFilterData}
onChange={(id) => {
setTemplateWapNotifId(id);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
selectedId={templateWapNotifId}
/>
<TextObjectFilter
placeholder="Alta de Reservas"
content={templateWapAltaFilterContent}
data={templateWapAltaFilterData}
onChange={(id) => {
setTemplateWapAltaId(id);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
selectedId={templateWapAltaId}
/>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,54 @@
"use client";
import { useEffect, useState, Suspense } from "react";
import { useSessionStore } from "@store/Sesion.Store";
import { useParams } from "next/navigation";
import Secure from "@components/Secure/Secure";
import OrganizationHeader from "@components/OrganizationHeader/OrganizationHeader";
import OrganizationContent from "@components/OrganizationContent/OrganizationContent";
import { loadOrganizationsById } from "./Org.Service";
import { MyOranizationsView } from "@models/Company.model";
import Menu from "@components/Menu/Menu";
import { useOrganizationHeaderStore } from "@core/Store/OrganizationHeader.Store";
import Content from "@core/app/components/Content/Content";
import SuspenseLoading from "@core/app/components/SuspendeLoading/SuspenseLoading";
type UrlData = {
id: string;
};
export default function OrgLayout({ children }: { children: React.ReactNode }) {
const [organization, setOrganization] = useState<MyOranizationsView | undefined>();
const data = useParams<UrlData>();
const organizationId = data.id;
const SessionInfo = useSessionStore();
const organizationHeader = useOrganizationHeaderStore();
useEffect(() => {
if (!SessionInfo.userId) {
return;
}
loadOrganizationsById({ id: organizationId, sessionUser: SessionInfo.userId }).then(
(organization) => {
setOrganization(organization);
organizationHeader.setHeaderColor(organization.headerColor);
organizationHeader.setHeaderFontColor(organization.headerFontColor);
organizationHeader.setHeaderFontShadowColor(organization.headerFontShadowColor);
}
);
}, [SessionInfo.userId]);
return (
<>
<Content>
<Suspense fallback={<SuspenseLoading />}>
<Secure>
{organization && <OrganizationHeader data={organization} />}
<OrganizationContent>{children}</OrganizationContent>
<Menu />
</Secure>
</Suspense>
</Content>
</>
);
}
@@ -0,0 +1,315 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import { useAlert } from "@core/Store/Alert.Store";
import { useSessionStore } from "@store/Sesion.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import PlaceOutlinedIcon from "@mui/icons-material/PlaceOutlined";
import ChatBubbleOutlineOutlinedIcon from "@mui/icons-material/ChatBubbleOutlineOutlined";
import CategoryOutlinedIcon from "@mui/icons-material/CategoryOutlined";
import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
import AdminPanelSettingsOutlinedIcon from "@mui/icons-material/AdminPanelSettingsOutlined";
import PendingActionsOutlinedIcon from "@mui/icons-material/PendingActionsOutlined";
import TodayOutlinedIcon from "@mui/icons-material/TodayOutlined";
import PaymentsIcon from "@mui/icons-material/CurrencyExchangeOutlined";
import CashIcon from "@mui/icons-material/AccountBalanceOutlined";
import DiscountsIcon from "@mui/icons-material/LocalOfferOutlined";
import TemplatesIcon from "@mui/icons-material/SpeakerNotesOutlined";
import AssistanceIcon from "@mui/icons-material/HeadsetMicOutlined";
import ShareOutlinedIcon from "@mui/icons-material/ShareOutlined";
import Dialog from "@mui/material/Dialog";
import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle";
import CloseIcon from "@mui/icons-material/Close";
import IconButton from "@mui/material/IconButton";
import { getSlug } from "@core/app/helpers/Slug";
import {
FacebookShareButton,
WhatsappShareButton,
TwitterShareButton,
EmailShareButton,
TelegramShareButton,
FacebookIcon,
WhatsappIcon as WhatsAppShareIcon,
TwitterIcon,
EmailIcon,
TelegramIcon
} from "react-share";
import { useNavigation } from "@hooks/goto";
import { MyOranizationsView } from "@models/Company.model";
import { loadOrganizationsById } from "./Org.Service";
import Button from "@components/Button/Button";
import MaterialAlert from "@mui/material/Alert";
import { validateGuestCollaborator } from "./collaborators/Collaborators.Service";
import { useBottomToolbarStore } from "@store/BottomToolbar.Store";
import dayjs from "dayjs";
import "dayjs/locale/es";
import { useAppointmentsViewStore } from "@core/Store/AppointmentsView.Store";
import ButtonPanel, { ButtonPanelItem } from "@core/app/components/ButtonPanel/ButtonPanel";
dayjs.locale("es");
type UrlData = {
id: string;
};
export default function OrganizationProfile() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const bottomToolbar = useBottomToolbarStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
const appointmentsView = useAppointmentsViewStore();
const { goTo } = useNavigation();
const [organization, setOrganization] = useState<MyOranizationsView | null>();
const [collaboratorButtons, setCollaboratorButtons] = useState<Array<ButtonPanelItem>>([]);
const buttons: Array<ButtonPanelItem> = [
{
icon: <StorefrontOutlinedIcon />,
text: "Configuración",
onClick: () => goTo("/admin/org/profile/" + id + "/edit"),
},
{
icon: <PlaceOutlinedIcon />,
text: "Editar ubicación",
onClick: () => goTo("/admin/org/profile/" + id + "/address"),
},
{
icon: <ChatBubbleOutlineOutlinedIcon />,
text: "WhatsApp",
onClick: () => goTo("/admin/org/profile/" + id + "/wap"),
},
{
icon: <CategoryOutlinedIcon />,
text: "Servicios",
onClick: () => goTo("/admin/org/profile/" + id + "/services"),
},
{
icon: <PeopleAltOutlinedIcon />,
text: "Colaboradores",
onClick: () => goTo("/admin/org/profile/" + id + "/collaborators"),
},
{
icon: <AdminPanelSettingsOutlinedIcon />,
text: "Clientes",
onClick: () => goTo("/admin/org/profile/" + id + "/clients"),
},
{
icon: <PendingActionsOutlinedIcon />,
text: "Horarios habilitados",
onClick: () => goTo("/admin/org/profile/" + id + "/schedules"),
},
{
icon: <TodayOutlinedIcon />,
text: "Turnos",
onClick: () => {
appointmentsView.clear();
goTo(
"/admin/org/profile/" +
id +
"/appointments/" +
encodeURIComponent(dayjs(new Date()).clone().startOf("day").toISOString())
);
},
},
{
icon: <PaymentsIcon />,
text: "Pagos",
onClick: () => goTo("/admin/org/profile/" + id + "/payments"),
},
{
icon: <CashIcon />,
text: "Caja",
onClick: () => goTo("/admin/org/profile/" + id + "/cash"),
},
{
icon: <DiscountsIcon />,
text: "Descuentos",
onClick: () => goTo("/admin/org/profile/" + id + "/discounts"),
},
{
icon: <TemplatesIcon />,
text: "Plantillas",
onClick: () => goTo("/admin/org/profile/" + id + "/templates"),
},
{
icon: <AssistanceIcon />,
text: "Asistencia",
onClick: () => goTo("/admin/org/profile/" + id + "/assistance"),
},
];
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Organizaciones",
icon: <StorefrontOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
location.add({
url: "",
title: "Panel de control",
icon: null,
});
bottomToolbar.set(<></>);
if (SessionInfo.userId) {
loadOrganization();
}
}, [SessionInfo.userId]);
useEffect(() => {
if (!organization) {
return;
}
if (!organization.employeeId) {
return;
}
setCollaboratorButtons([
{
icon: <TodayOutlinedIcon />,
text: "Mis Turnos",
onClick: () => {
appointmentsView.setCollaboratorId(organization.employeeId);
goTo(
"/admin/org/profile/" +
id +
"/appointments/" +
encodeURIComponent(
dayjs(new Date()).clone().startOf("day").toISOString()
)
);
},
},
{
icon: <ChatBubbleOutlineOutlinedIcon />,
text: "Mensajes",
onClick: () => goTo("/messenger"),
},
]);
}, [organization]);
const loadOrganization = () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
loadOrganizationsById({ id: id, sessionUser: SessionInfo.userId })
.then((response: MyOranizationsView) => {
setOrganization(response);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const handleAgree = () => {
if (!organization) {
return;
}
eventHandler.setEventType(EVENT_TYPES.LOADING);
validateGuestCollaborator({
employeeId: organization.employeeId,
sessionUser: SessionInfo.userId,
})
.then(() => {
loadOrganization();
})
.catch((error) => {
alert.showError(error.format());
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
return (
<>
<Location />
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: "20px",
gap: "10px",
}}
>
{organization && (organization.isOwner || organization.isAdmin) && (
<>
<ButtonPanel title="Área de administración" items={buttons} />
<div style={{ height: "15px" }}></div>
</>
)}
{organization && !organization.fullOk && (
<>
<h3>Área de Colaboradores</h3>
<div
style={{
display: "flex",
flexDirection: "column",
alignContent: "center",
justifyContent: "center",
width: "100%",
}}
>
<MaterialAlert
severity="info"
sx={{
marginTop: "10px",
maxWidth: "680px",
marginLeft: "auto",
marginRight: "auto",
}}
>
<strong>Atención:</strong> Para formar parte de la organización
primero debes aceptar la invitación y esperar a ser aceptado por el
administrador
</MaterialAlert>
{!organization.guestOk && (
<Button
name="acceptButton"
width="100%"
text="Aceptar invitación"
style={{
marginTop: "20px",
maxWidth: "680px",
marginLeft: "auto",
marginRight: "auto",
}}
onClick={handleAgree}
/>
)}
</div>
</>
)}
{organization && organization.fullOk && (
<>
<ButtonPanel title="Área de Colaboradores" items={collaboratorButtons} />
</>
)}
<div style={{ height: "20px" }}></div>
</div>
</>
);
}
@@ -0,0 +1,692 @@
"use client";
import { useParams } from "next/navigation";
import { useState, useEffect, useRef } from "react";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import { useSessionStore } from "@store/Sesion.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import PaymentsIcon from "@mui/icons-material/CurrencyExchangeOutlined";
import { ThemeProvider } from "@emotion/react";
import turnosXpressTheme from "@core/app/theme/turnosXpress";
import { IconButton } from "@mui/material";
import ButtonMaterial from "@mui/material/Button";
import {
FindPaymentsParams,
PaginatePaymentsView,
PaginatePaymentsViewParams,
PaginatePaymentsViewResults,
PAYMENT_METHODS,
paymentMethods,
} from "@core/Models/Payments.model";
import dayjs, { Dayjs } from "dayjs";
import { formatPrice } from "@core/app/helpers/Numbers";
import { loadPaymentsView, loadPaymentsViewTotal, deletePayment } from "../Org.Service";
import { getPaymentIcon } from "@core/app/components/PaymentIcons/PaymentIcons";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import TextObjectMultiFilter from "@core/app/components/TextObjectMultiFilter/TextObjectMultiFilter";
import { useFilterStore } from "@core/Store/Filter.Store";
import { FindEmployeesParams } from "@core/Models/Collaborators.model";
import filterObjects from "@components/TextObjectFilter/Filter.Service";
import { FindServicesParams } from "@core/Models/Service.model";
import { FindClientsParams } from "@core/Models/Clients.model";
import TextRangeDateModern from "@core/app/components/TextRangeDateModern/TextRangeDateModern";
import { useNavigation } from "@core/app/hooks/goto";
import DeleteIcon from "@mui/icons-material/DeleteOutlineOutlined";
import PictureAsPdfIcon from "@mui/icons-material/PictureAsPdf";
import { useAlert } from "@core/Store/Alert.Store";
import { useConfirmStore } from "@core/Store/Confirm.Store";
import { GetPaymentsReport } from "../Org.Service";
import SecureByPlan, { SECURE_CONTENTS } from "@core/app/components/SecureByPlan/SecureByPlan";
import { motion, AnimatePresence } from "motion/react";
import Pagination from "@mui/material/Pagination";
import SearchOffIcon from "@mui/icons-material/SearchOff";
import UnfoldLessIcon from "@mui/icons-material/UnfoldLess";
import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore";
import FilterAltOutlinedIcon from "@mui/icons-material/FilterAltOutlined";
type UrlData = {
id: string;
};
interface FilterContentProps {
collaboratorsData: TextObjectFilterResult[];
setCollaboratorId: (id: string) => void;
servicesData: TextObjectFilterResult[];
setServiceId: (id: string) => void;
methodsData: TextObjectFilterResult[];
setMethodId: (id: string) => void;
clientData: TextObjectFilterResult[];
setClientIds: (ids: string[]) => void;
clientIds: string[];
}
const FilterContent = (props: FilterContentProps) => {
const [clientFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Cliente:</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del cliente para buscarlo.</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [collaboratorFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Colaborador:</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del colaborador para buscarlo.</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [serviceFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Servicio:</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del servicio para buscarlo.</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [methodFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div style={{ fontWeight: "700", fontSize: "14px", paddingTop: "7px" }}>Seleccionar Método de Pago:</div>
<p style={{ fontWeight: "500", fontSize: "13px", paddingTop: "2px" }}>Ingrese el nombre del método de pago para buscarlo.</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
return (
<>
<TextObjectMultiFilter
placeholder="Cliente"
content={clientFilterContent}
data={props.clientData}
onChange={(ids) => props.setClientIds(ids)}
selectedIds={props.clientIds}
/>
<TextObjectFilter
placeholder="Colaborador"
content={collaboratorFilterContent}
data={props.collaboratorsData}
onChange={(id) => props.setCollaboratorId(id)}
/>
<TextObjectFilter
placeholder="Servicio"
content={serviceFilterContent}
data={props.servicesData}
onChange={(id) => props.setServiceId(id)}
/>
<TextObjectFilter
placeholder="Método de Pago"
content={methodFilterContent}
data={props.methodsData}
onChange={(id) => props.setMethodId(id)}
/>
</>
);
};
export default function OrganizationEdit() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const filter = useFilterStore();
const alert = useAlert();
const confirm = useConfirmStore();
const { goTo } = useNavigation();
const [paymentsTotal, setPaymentsTotal] = useState<number>(0);
const [dateFrom, setDateFrom] = useState<Dayjs>(dayjs().startOf("day"));
const [dateTo, setDateTo] = useState<Dayjs>(dayjs().endOf("day"));
const [reloadPayments, setReloadPayments] = useState<boolean>(false);
const [clientFilterData, setClientFilterData] = useState<TextObjectFilterResult[]>([]);
const [clientIds, setClientIds] = useState<string[]>([]);
const [collaboratorFilterData, setCollaboratorFilterData] = useState<TextObjectFilterResult[]>([]);
const [collaboratorId, setCollaboratorId] = useState<string>("");
const [serviceFilterData, setServiceFilterData] = useState<TextObjectFilterResult[]>([]);
const [serviceId, setServiceId] = useState<string>("");
const [methodFilterData, setMethodFilterData] = useState<TextObjectFilterResult[]>([]);
const [methodId, setMethodId] = useState<string>("");
const [pageSize] = useState<number>(10);
const [payments, setPayments] = useState<PaginatePaymentsViewResults>();
const paymentsRef = useRef(payments);
const [isScrolled, setIsScrolled] = useState(false);
const [forceMinimized, setForceMinimized] = useState(false);
const sentinelRef = useRef<HTMLDivElement>(null);
const isMinimized = forceMinimized || isScrolled;
const generarReporte = async () => {
try {
const pdfMake = await import("pdfmake/build/pdfmake");
const pdfFonts = (await import("pdfmake/build/vfs_fonts")) as any;
pdfMake.vfs = pdfFonts.pdfMake ? pdfFonts.pdfMake.vfs : pdfFonts.vfs;
const report = await GetPaymentsReport({
companyId: id,
startDate: dateFrom.toDate(),
endDate: dateTo.toDate(),
clientIds: clientIds.length > 0 ? clientIds : undefined,
serviceId: serviceId ? serviceId : undefined,
employeeId: collaboratorId ? collaboratorId : undefined,
method: methodId ? (methodId as any) : undefined,
sessionUser: SessionInfo.userId
});
if (!report.items || report.items.length === 0) {
alert.showError("No hay datos para generar el reporte");
return;
}
let logoDataUrl = "";
try {
const response = await fetch("/favicon512.png");
const blob = await response.blob();
logoDataUrl = await new Promise((resolve) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result as string);
reader.readAsDataURL(blob);
});
} catch(e) {}
const tableBody: any[] = [
[
{ text: "Cliente", bold: true, color: "#333333", fillColor: "#f5f5f5" },
{ text: "Datos del Turno", bold: true, color: "#333333", fillColor: "#f5f5f5" },
{ text: "Monto", bold: true, color: "#333333", fillColor: "#f5f5f5" }
]
];
report.items.forEach(item => {
tableBody.push([
item.clientName || "-",
{
text: [
{ text: `${item.serviceName || "-"}\n`, bold: true, fontSize: 11 },
`Prof: ${item.employeeName || "-"}\nFecha: ${dayjs(item.appointmentDate).format("DD/MM/YYYY HH:mm")} hs\nMétodo: ${item.method || "-"}`
],
fontSize: 10
},
{ text: "$" + item.price.toLocaleString("es-AR"), alignment: "right" }
]);
});
tableBody.push([
{ text: "TOTAL PROCESADO", bold: true, colSpan: 2, alignment: "right" },
{},
{ text: "$" + report.totalAmount.toLocaleString("es-AR"), bold: true, alignment: "right" }
]);
const docDefinition: unknown = {
content: [
{
columns: [
{ text: "Reporte de Cobros Procesados", fontSize: 18, bold: true, margin: [0, 0, 0, 10], width: '*' },
...(logoDataUrl ? [{ image: logoDataUrl, width: 45, margin: [0, 0, 0, 10], alignment: 'right' }] : [])
]
},
{ text: `Filtros aplicados:`, bold: true, margin: [0, 0, 0, 5] },
{
text: [
{ text: "Clientes Seleccionados: ", bold: true },
`${clientIds.length > 0 ? clientIds.length : "Todos"}`
],
margin: [0, 0, 0, 2]
},
{
text: [
{ text: "Período: ", bold: true },
`${dateFrom ? dateFrom.format("DD/MM/YYYY") : "Inicio"} al ${dateTo ? dateTo.format("DD/MM/YYYY") : "Fin"}`
],
margin: [0, 0, 0, 20]
},
{
table: {
headerRows: 1,
widths: [120, "*", "auto"],
body: tableBody
},
layout: "lightHorizontalLines"
}
]
};
if ((window as unknown as { Android: any }).Android && (window as unknown as { Android: any }).Android.savePdf) {
pdfMake.createPdf(docDefinition as any).getBase64((data: any) => {
(window as unknown as { Android: any }).Android?.savePdf(data, `reporte-procesados.pdf`);
});
} else {
pdfMake.createPdf(docDefinition as any).download(`reporte-procesados.pdf`);
}
} catch (error: unknown) {
console.error(error);
alert.showError((error as { format?: () => string }).format ? (error as { format: () => string }).format() : "Error generando PDF");
}
};
useEffect(() => {
const handleScroll = (e: Event) => {
const target = e.target as HTMLElement;
// Check if it's a DOM element scrolling
if (target && target.scrollTop !== undefined) {
// Ignore small scroll containers like dropdowns by ensuring the container is large
if (target.clientHeight > 300) {
setIsScrolled(target.scrollTop > 50);
}
} else if ((target as unknown) === document || (target as unknown) === window) {
// If it's the window/document scrolling
setIsScrolled(window.scrollY > 50);
}
};
// Use capture: true to catch all scroll events in the app regardless of where they happen
window.addEventListener("scroll", handleScroll, { capture: true, passive: true });
return () => {
window.removeEventListener("scroll", handleScroll, { capture: true });
};
}, []);
const loadPayments = (page: number = 1, pageSize: number = 30) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
const filterData: PaginatePaymentsViewParams = {
page: page,
limit: pageSize,
companyId: id,
sessionUser: SessionInfo.userId,
};
if (dateFrom) filterData.startDate = dateFrom.toDate();
if (dateTo) filterData.endDate = dateTo.toDate();
if (clientIds && clientIds.length > 0) filterData.clientIds = clientIds;
if (collaboratorId) filterData.employeeId = collaboratorId;
if (serviceId) filterData.serviceId = serviceId;
if (methodId) filterData.method = methodId as PAYMENT_METHODS;
loadPaymentsView(filterData)
.then((res) => {
if (res.page > res.pages) {
const newPage = res.page - 1;
if (newPage > 0) {
loadPayments(newPage, pageSize);
}
return;
}
setPayments(res);
setReloadPayments(false);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.CLEAR);
});
};
const loadPaymentsTotal = () => {
const filterData: FindPaymentsParams = {
companyId: id,
sessionUser: SessionInfo.userId,
};
if (dateFrom) filterData.startDate = dateFrom.toDate();
if (dateTo) filterData.endDate = dateTo.toDate();
if (clientIds && clientIds.length > 0) filterData.clientIds = clientIds;
if (collaboratorId) filterData.employeeId = collaboratorId;
if (serviceId) filterData.serviceId = serviceId;
if (methodId) filterData.method = methodId as PAYMENT_METHODS;
loadPaymentsViewTotal(filterData)
.then((res) => setPaymentsTotal(res))
.catch(() => setPaymentsTotal(0));
};
const deletePaymentHandler = (paymentId: string) => {
if (!paymentId || !SessionInfo.userId) return;
eventHandler.setEventType(EVENT_TYPES.LOADING);
deletePayment({
id: paymentId,
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("Pago eliminado");
setReloadPayments(true);
})
.catch((error) => alert.showError(error.format()))
.finally(() => eventHandler.setEventType(EVENT_TYPES.CLEAR));
};
useEffect(() => {
const cFilter: FindClientsParams = { companyId: id };
filterObjects<FindClientsParams>("clients/filter-all", cFilter).then((res) => setClientFilterData(res));
const eFilter: FindEmployeesParams = { companyId: id };
filterObjects<FindEmployeesParams>("employees/filter-all", eFilter).then((res) => setCollaboratorFilterData(res));
const sFilter: FindServicesParams = { companyId: id };
filterObjects<FindServicesParams>("services/filter-all", sFilter).then((res) => setServiceFilterData(res));
setMethodFilterData(paymentMethods.map((m) => ({ id: m.id, text: m.name, name: m.name, image: "" })));
}, []);
useEffect(() => {
filter.setTitle("Filtrar Pagos");
filter.clearFilterData();
filter.setContent(
<FilterContent
collaboratorsData={collaboratorFilterData}
setCollaboratorId={setCollaboratorId}
servicesData={serviceFilterData}
setServiceId={setServiceId}
methodsData={methodFilterData}
setMethodId={setMethodId}
clientData={clientFilterData}
setClientIds={setClientIds}
clientIds={clientIds}
/>
);
filter.setOnFilter(() => { });
}, [collaboratorFilterData, serviceFilterData, methodFilterData, clientFilterData]);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
location.clear();
location.add({ url: "/admin/org", title: "Organizaciones", icon: null });
location.add({ url: "/admin/org/profile/" + id, title: "Perfil", icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" /> });
location.add({ url: "/admin/org/profile/" + id, title: "Pagos", icon: <PaymentsIcon sx={{ fontSize: "32px", mr: "10px" }} /> });
}, []);
useEffect(() => {
paymentsRef.current = payments;
}, [payments]);
useEffect(() => {
const loadPage = paymentsRef.current?.page || 1;
loadPayments(loadPage, pageSize);
loadPaymentsTotal();
}, [clientIds, collaboratorId, serviceId, methodId, dateFrom, dateTo, reloadPayments]);
const dataList = payments?.data || [];
return (
<>
<Location />
<SecureByPlan content={SECURE_CONTENTS.PAYMENTS} companyId={id}>
<div style={{ maxWidth: "800px", margin: "0 auto", paddingBottom: "100px", marginTop: "0px" }}>
{/* Sentinel element to detect scroll */}
<div ref={sentinelRef} style={{ height: "1px", width: "100%" }}></div>
{/* Top Status Card */}
<div style={{ position: "sticky", top: "70px", zIndex: 100, marginBottom: "50px", display: "flex", flexDirection: "column", background: "white", paddingBottom: "10px", paddingTop: "5px", margin: "0 -10px", paddingLeft: "10px", paddingRight: "10px" }}>
{/* Tabs Selector */}
<div style={{ display: "flex", justifyContent: "center", marginBottom: "15px" }}>
<ThemeProvider theme={turnosXpressTheme}>
<div style={{
display: "flex",
background: "rgba(255,255,255,0.9)",
backdropFilter: "blur(10px)",
borderRadius: "30px",
padding: "5px",
boxShadow: "0 4px 15px rgba(0,0,0,0.05)"
}}>
<ButtonMaterial
variant="contained"
style={{ borderRadius: "25px", boxShadow: "0 2px 8px rgba(0,0,0,0.1)", fontWeight: "bold", padding: "8px 25px" }}
>
Pagados
</ButtonMaterial>
<ButtonMaterial
variant="text"
onClick={() => goTo(`/admin/org/profile/${id}/payments/not-payed`)}
style={{ borderRadius: "25px", color: "#666", fontWeight: "bold", padding: "8px 25px" }}
>
Adeudados
</ButtonMaterial>
</div>
</ThemeProvider>
</div>
<motion.div
layout
initial={{ opacity: 0, y: -20 }}
animate={{
opacity: 1,
y: 0,
padding: isMinimized ? "25px 50px" : "40px 20px",
borderRadius: isMinimized ? "15px" : "20px"
}}
style={{
background: "linear-gradient(135deg, #4b6cb7 0%, #182848 100%)",
color: "white",
textAlign: "center",
boxShadow: isMinimized ? "0 4px 15px rgba(0,0,0,0.3)" : "0 10px 30px rgba(0,0,0,0.15)",
position: "relative",
display: "flex",
flexDirection: isMinimized ? "row" : "column",
alignItems: "center",
justifyContent: isMinimized ? "space-between" : "center",
transition: "box-shadow 0.3s ease"
}}
>
<IconButton
onClick={() => setForceMinimized(!forceMinimized)}
style={{ position: "absolute", top: "10px", left: "10px", color: "rgba(255,255,255,0.7)", zIndex: 10 }}
size="small"
title={forceMinimized ? "Maximizar" : "Minimizar"}
>
{forceMinimized ? <UnfoldMoreIcon /> : <UnfoldLessIcon />}
</IconButton>
<IconButton
onClick={() => filter.toggle()}
style={{ position: "absolute", top: "10px", right: "10px", color: "rgba(255,255,255,0.7)", zIndex: 10 }}
size="small"
title="Filtrar"
>
<FilterAltOutlinedIcon />
</IconButton>
<div style={{ position: "absolute", top: "-50px", right: "-50px", width: "150px", height: "150px", borderRadius: "50%", background: "rgba(255,255,255,0.05)" }}></div>
<div style={{ position: "absolute", bottom: "-30px", left: "-30px", width: "100px", height: "100px", borderRadius: "50%", background: "rgba(255,255,255,0.05)" }}></div>
<motion.div
animate={{
fontSize: isMinimized ? "14px" : "16px",
marginBottom: isMinimized ? "0px" : "10px"
}}
style={{ fontWeight: "bold", textTransform: "uppercase", letterSpacing: "2px", opacity: 0.9 }}
>
Total Recaudado
</motion.div>
<motion.div
animate={{
fontSize: isMinimized ? "24px" : "56px",
margin: isMinimized ? "0px" : "10px 0"
}}
style={{ fontWeight: "900" }}
>
{formatPrice(paymentsTotal)}
</motion.div>
{!isMinimized && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
style={{ fontSize: "14px", opacity: 0.8 }}
>
<div style={{ display: "flex", gap: "10px", alignItems: "center", flexWrap: "wrap", justifyContent: "center", marginTop: "10px" }}>
<div style={{ width: "250px", maxWidth: "100%" }}>
<TextRangeDateModern
label="Período"
dateFrom={dateFrom}
dateTo={dateTo}
setDateFrom={setDateFrom}
setDateTo={setDateTo}
/>
</div>
<ButtonMaterial
variant="outlined"
onClick={generarReporte}
style={{
color: "white",
borderColor: "rgba(255,255,255,0.5)",
fontWeight: "bold",
borderRadius: "20px",
padding: "8px 20px",
textTransform: "none",
height: "56px"
}}
>
<PictureAsPdfIcon style={{ fontSize: "18px", marginRight: "5px" }} /> PDF
</ButtonMaterial>
</div>
</motion.div>
)}
</motion.div>
</div>
{/* Dynamic Spacer for layout shift */}
<div style={{ height: isMinimized ? "100px" : "0px", transition: "height 0.3s ease" }}></div>
{/* Payments List */}
<div style={{ display: "flex", flexDirection: "column", gap: "12px", paddingTop: "15px" }}>
{dataList.length > 0 ? (
<AnimatePresence>
{dataList.map((payment, index) => (
<motion.div
key={payment.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2, delay: index * 0.03 }}
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: "#fff",
borderRadius: "15px",
padding: "16px 20px",
boxShadow: "0 4px 12px rgba(0,0,0,0.05)",
borderLeft: "5px solid #28a745",
position: "relative"
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "15px", flex: 1 }}>
<div style={{
width: "50px",
height: "50px",
borderRadius: "12px",
backgroundColor: "rgba(40, 167, 69, 0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#28a745"
}}>
{getPaymentIcon(payment.method)}
</div>
<div style={{ flex: 1 }}>
<div style={{ fontSize: "16px", fontWeight: "bold", color: "#333", marginBottom: "2px" }}>
{payment.clientName}
</div>
<div style={{ fontSize: "14px", color: "var(--wine-dark)", fontWeight: "bold", marginBottom: "4px" }}>
{payment.serviceName}
</div>
<div style={{ display: "flex", gap: "10px", fontSize: "12px", color: "#888" }}>
<span>{dayjs(payment.createdAt).format("DD MMMM YYYY • HH:mm")}</span>
<span></span>
<span>{payment.employeeName}</span>
</div>
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "15px" }}>
<div style={{
fontSize: "20px",
fontWeight: "900",
color: "#28a745",
textAlign: "right"
}}>
+{formatPrice(payment.price)}
</div>
<IconButton
color="error"
size="small"
onClick={() => {
confirm.show(
"¿Estás seguro que quieres eliminar el pago de " + payment.clientName + "?",
() => deletePaymentHandler(payment.id),
() => { }
);
}}
sx={{
backgroundColor: "rgba(220, 53, 69, 0.05)",
"&:hover": { backgroundColor: "rgba(220, 53, 69, 0.15)" }
}}
>
<DeleteIcon fontSize="small" />
</IconButton>
</div>
</motion.div>
))}
</AnimatePresence>
) : (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
style={{ textAlign: "center", padding: "60px 20px", backgroundColor: "#fff", borderRadius: "15px", border: "1px dashed #ccc" }}
>
<SearchOffIcon sx={{ fontSize: "64px", color: "#ccc", marginBottom: "15px" }} />
<h2 style={{ margin: 0, color: "#999" }}>Sin Pagos</h2>
<p style={{ color: "#aaa" }}>No se encontraron pagos registrados para este período.</p>
</motion.div>
)}
</div>
{payments && payments.pages > 1 && (
<div style={{ display: "flex", justifyContent: "center", marginTop: "40px" }}>
<Pagination
count={payments.pages}
page={payments.page}
onChange={(e, value) => loadPayments(value, pageSize)}
color="primary"
/>
</div>
)}
</div>
</SecureByPlan>
</>
);
}
@@ -0,0 +1,12 @@
.collaboratorItem {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.collaboratorName {
padding: 10px;
width: 100%;
font-weight: 500;
}
@@ -0,0 +1,5 @@
"use client";
export default function ScheduleLayout({ children }: { children: React.ReactNode }) {
return <>{children}</>;
}
@@ -0,0 +1,176 @@
"use client";
import { useState, useEffect } from "react";
import { useParams } from "next/navigation";
import style from "./Schedules.module.css";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import { useAlert } from "@core/Store/Alert.Store";
import { useSessionStore } from "@store/Sesion.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import { disableSchedule, enableSchedule, loadSchedulesEnabled } from "../Org.Service";
import WeekInput from "@core/app/components/WeekInput/WeekInput";
import dayjs from "dayjs";
import "dayjs/locale/es";
import weekday from "dayjs/plugin/weekday";
import "dayjs/locale/es";
import { SchedulesEnabledView } from "@models/SchedulesEnabled.type";
import Avatar from "@components/Avatar/Avatar";
import Button from "@core/app/components/Button/Button";
import { useConfirmStore } from "@core/Store/Confirm.Store";
dayjs.extend(weekday);
dayjs.locale("es");
type UrlData = {
id: string;
};
export default function Schedules() {
const data = useParams<UrlData>();
const id = data.id;
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const SessionInfo = useSessionStore();
const alert = useAlert();
const confirm = useConfirmStore();
const [startDate, setStartDate] = useState<dayjs.Dayjs>(dayjs().weekday(0));
const [collaborators, setCollaborators] = useState<SchedulesEnabledView[]>([]);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Organización",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "",
title: "Horarios Habilitados",
icon: null,
});
}, []);
useEffect(() => {
loadCollaborators();
}, [startDate]);
const loadCollaborators = async () => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
loadSchedulesEnabled({
companyId: id,
startDate: startDate.format("YYYY-MM-DD"),
endDate: startDate.clone().add(6, "d").format("YYYY-MM-DD"),
})
.then((res) => {
setCollaborators(res);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const enable = (collaborator: SchedulesEnabledView) => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
enableSchedule({
companyId: id,
employees: [{ employeeId: collaborator.employeeId }],
startDate: startDate.format("YYYY-MM-DD"),
endDate: startDate.clone().add(6, "d").format("YYYY-MM-DD"),
sessionUser: SessionInfo.userId,
})
.then(() => {
loadCollaborators();
alert.showSuccess("Horario habilitado");
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const disable = (collaborator: SchedulesEnabledView) => {
confirm.show(
"Si continua se deshabilitarán las reservas en el periodo seleccionado. ¿Desea anular las reservas de " +
collaborator.employeeFullName +
" para este periodo?",
() => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
disableSchedule({
companyId: id,
employees: [{ employeeId: collaborator.employeeId }],
startDate: startDate.format("YYYY-MM-DD"),
endDate: startDate.clone().add(6, "d").format("YYYY-MM-DD"),
sessionUser: SessionInfo.userId,
})
.then(() => {
loadCollaborators();
alert.showSuccess("Horario anulado");
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
},
() => {
return;
}
);
};
return (
<>
<Location />
<div
style={{ display: "flex", flexDirection: "column", marginTop: "20px", gap: "10px" }}
>
<WeekInput setStartDate={setStartDate} />
{collaborators.map((collaborator) => (
<div
className={style.collaboratorItem}
key={"collaborator." + collaborator.employeeId}
>
<Avatar
src={collaborator.employeeAvatar}
name="avatar"
size="super-small"
border="none"
alt={collaborator.employeeFullName}
style={{ minWidth: "32px" }}
/>
<span className={style.collaboratorName}>
{collaborator.employeeFullName}
</span>
{collaborator.enabled && (
<Button
text="Anular"
width="custom"
color="secondary"
style={{ minWidth: "100px" }}
onClick={() => disable(collaborator)}
/>
)}
{!collaborator.enabled && (
<Button
text="Habilitar"
width="custom"
color="success"
style={{ minWidth: "100px" }}
onClick={() => enable(collaborator)}
/>
)}
</div>
))}
</div>
</>
);
}
@@ -0,0 +1,161 @@
import {
CreateServiceParams,
UpdateServiceParams,
IService,
CompanyServiceView,
SetPublishedStatusParams,
ApplyDiscountParams,
DeleteDiscountParams,
} from "@models/Service.model";
import ApiRequest from "@services/Api.Service";
import { ApiVoidResult } from "@models/Api.VoidResult.type";
import * as Yup from "yup";
const schema = Yup.object().shape({
name: Yup.string().required("Debe introducir el nombre del servicio."),
description: Yup.string().required("Debe introducir la descripción del servicio."),
originalPrice: Yup.number()
.required("Debe introducir el precio del servicio.")
.typeError("El precio debe ser un número.")
.positive("El precio debe ser un número positivo."),
limit: Yup.number()
.required("Debe introducir el limite del servicio.")
.typeError("El limite debe ser un número.")
.positive("El limite debe ser un número positivo."),
length: Yup.number()
.required("Debe introducir la duración del servicio.")
.typeError("La duración debe ser un número expresado en minutos.")
.positive("El La duración debe ser un número positivo expresado en minutos."),
sessionUser: Yup.string().required("Para crear una organización debe iniciar sesión."),
});
const schemaApplyDiscount = Yup.object().shape({
discountId: Yup.string().required("No se ha seleccionado un descuento para aplicar."),
companyId: Yup.string().required(
"Para aplicar el descuento debe seleccionar una organización."
),
serviceId: Yup.string().required("Para aplicar el descuento debe seleccionar un servicio."),
sessionUser: Yup.string().required("Para crear una organización debe iniciar sesión."),
});
const schemaDeleteDiscount = Yup.object().shape({
companyId: Yup.string().required(
"Para aplicar el descuento debe seleccionar una organización."
),
serviceId: Yup.string().required("Para aplicar el descuento debe seleccionar un servicio."),
sessionUser: Yup.string().required("Para crear una organización debe iniciar sesión."),
});
export const savePublishedStatus = async (
data: SetPublishedStatusParams
): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("services/set-published-status", data);
};
export const loadServicesByCompany = async (companyId: string): Promise<CompanyServiceView[]> => {
return ApiRequest.post<CompanyServiceView[]>("services/get-by-company", {
companyId: companyId,
});
};
export const loadServicesById = async (serviceId: string): Promise<CompanyServiceView> => {
return ApiRequest.post<CompanyServiceView>("services/get-by-id", {
serviceId: serviceId,
});
};
export const createService = async (data: CreateServiceParams): Promise<IService> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schema.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new Error(yupErrors.message);
}
return ApiRequest.post<IService>("services/create", data);
};
export const updateService = async (data: UpdateServiceParams): Promise<ApiVoidResult> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schema.validate(data, { abortEarly: true });
if (!data.id || data.id == "") {
throw new Yup.ValidationError("Servicio desconocido. No es posible actualizar");
}
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new Error(yupErrors.message);
}
return ApiRequest.post<ApiVoidResult>("services/update", data);
};
export const uploadImage = async (data: FormData): Promise<ApiVoidResult> => {
return ApiRequest.postFile<ApiVoidResult>("services/upload-image", data);
};
export const applyDiscount = async (data: ApplyDiscountParams): Promise<ApiVoidResult> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaApplyDiscount.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new Error(yupErrors.message);
}
return ApiRequest.post<ApiVoidResult>("services/apply-discount", data);
};
export const deleteDiscount = async (data: DeleteDiscountParams): Promise<ApiVoidResult> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaDeleteDiscount.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new Error(yupErrors.message);
}
return ApiRequest.post<ApiVoidResult>("services/delete-discount", data);
};
@@ -0,0 +1,10 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function OrgLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}
@@ -0,0 +1,672 @@
"use client";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useSessionStore } from "@store/Sesion.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import Textbox from "@components/Textbox/Textbox";
import { useAlert } from "@store/Alert.Store";
import MaterialAlert from "@mui/material/Alert";
import { useNavigation } from "@hooks/goto";
import { useEffect, useState } from "react";
import {
loadServicesById,
createService,
updateService,
uploadImage,
savePublishedStatus,
applyDiscount,
deleteDiscount,
} from "../Services.Service";
import {
ApplyDiscountParams,
CompanyServiceView,
DeleteDiscountParams,
SERVICE_PUBLISHED_STATUS,
SetPublishedStatusParams,
} from "@core/Models/Service.model";
import ImagePicker from "@core/app/components/ImagePicker/ImagePicker";
import { PopoverPicker } from "@core/app/components/ColorPicker/ColorPicker";
import Switch from "@core/app/components/Switch/Switch";
import { Grid2 } from "@mui/material";
import ButtonMaterial from "@mui/material/Button";
import DiscountsIcon from "@mui/icons-material/LocalOfferOutlined";
import { useBottomToolbarStore } from "@core/Store/BottomToolbar.Store";
import { DiscountType, FindDiscountsParams } from "@core/Models/Discounts.model";
import { formatPrice } from "@core/app/helpers/Numbers";
import { TextObjectModalContent } from "@core/Store/TextObjectModal.Store";
import { TextObjectFilterResult } from "@core/Models/TextObjectFilter.model";
import TextObjectFilterItem from "@core/app/components/TextObjectFilterItem/TextObjectFilterItem";
import TextObjectFilter from "@core/app/components/TextObjectFilter/TextObjectFilter";
import filterObjects from "@components/TextObjectFilter/Filter.Service";
import DialogWindow from "@core/app/components/DialogWindow/DialogWindow";
import { DialogActions, DialogContent } from "@mui/material";
import Link from "next/link";
import { useConfirmStore } from "@core/Store/Confirm.Store";
type UrlData = {
id: string;
sid: string;
};
export default function OrganizationService() {
const data = useParams<UrlData>();
const id = data.id;
const sid = data.sid;
const SessionInfo = useSessionStore();
const eventHandler = useEventHandlerStore();
const location = useLocationStore();
const alert = useAlert();
const confirm = useConfirmStore();
const bottomToolbar = useBottomToolbarStore();
const { goTo } = useNavigation();
const [serviceName, setServiceName] = useState("");
const [serviceDescription, setServiceDescription] = useState("");
const [serviceOriginalPrice, setServiceOriginalPrice] = useState("");
const [serviceLimit, setServiceLimit] = useState("");
const [serviceLength, setServiceLength] = useState("");
const [serviceImage, setServiceImage] = useState("");
const [serviceColor, setServiceColor] = useState("");
const [serviceFontColor, setServiceFontColor] = useState("");
const [serviceFontShadowColor, setServiceFontShadowColor] = useState("");
const [serviceDiscountType, setServiceDiscountType] = useState<string | undefined>(undefined);
const [serviceDiscountValue, setServiceDiscountValue] = useState<number | undefined>(undefined);
const [serviceDiscountName, setServiceDiscountName] = useState<string | undefined>(undefined);
const [servicePublished, setServicePublished] = useState<SERVICE_PUBLISHED_STATUS | undefined>(
undefined
);
const [savePublishedStatusAvailable, setSavePublishedStatusAvailable] = useState(false);
const [dialogDiscountVisible, setDialogDiscountVisible] = useState(false);
const [discountsFilterContent] = useState<TextObjectModalContent>({
header: (
<div>
<div
style={{
fontWeight: "700",
fontSize: "14px",
paddingTop: "7px",
}}
>
Seleccionar Descuento:
</div>
<p
style={{
fontWeight: "500",
fontSize: "13px",
paddingTop: "2px",
}}
>
Ingrese el nombre del descuento para buscarlo.
</p>
</div>
),
bottom: <div>&nbsp;</div>,
renderResult: (result: TextObjectFilterResult, index: number) => (
<TextObjectFilterItem key={index} data={result} />
),
});
const [discountsFilterData, setDiscountsFilterData] = useState<TextObjectFilterResult[]>([]);
const [discountId, setDiscountId] = useState<string>("");
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
bottomToolbar.set(
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "row",
fontWeight: "bold",
color: "var(--white)",
padding: "10px",
width: "100%",
fontSize: "22px",
paddingRight: "50px",
}}
>
<ButtonMaterial
variant="text"
startIcon={<DiscountsIcon />}
sx={{ color: "var(--white)", marginLeft: "30px" }}
onClick={() => {
setDialogDiscountVisible(true);
}}
>
Aplicar Descuento
</ButtonMaterial>
</div>
);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org/profile/" + id,
title: "Organizacion",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id + "/services",
title: "Servicios",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org/services",
title: sid === "new" ? "Crear servicio" : "Editar Servicio",
icon: <StorefrontOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
if (sid !== "new") {
eventHandler.setEventType(EVENT_TYPES.LOADING);
cargarDatos();
}
}, []);
useEffect(() => {
const filter: FindDiscountsParams = {
companyId: id,
sessionUser: SessionInfo.userId,
};
filterObjects<FindDiscountsParams>("discounts/filter-all", filter).then((res) => {
setDiscountsFilterData(res);
});
}, [SessionInfo.userId]);
useEffect(() => {
if (eventHandler.eventType === EVENT_TYPES.SAVE) {
//validar datos
if (sid === "new") {
//crear nuevo servicio.
createService({
companyId: id,
name: serviceName,
description: serviceDescription,
originalPrice: parseFloat(serviceOriginalPrice),
limit: parseInt(serviceLimit),
length: parseInt(serviceLength),
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("Servicio creado");
goTo("/admin/org/profile/" + id + "/services");
})
.catch((error) => {
alert.showError(error.message);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
} else {
//editar servicio actual.
updateService({
id: sid,
companyId: id,
name: serviceName,
description: serviceDescription,
originalPrice: parseFloat(serviceOriginalPrice),
limit: parseInt(serviceLimit),
length: parseInt(serviceLength),
color: serviceColor,
fontColor: serviceFontColor,
fontShadowColor: serviceFontShadowColor,
sessionUser: SessionInfo.userId,
})
.then(() => {
alert.showSuccess("Servicio Actualizado");
})
.catch((error: Error) => {
alert.showError(error.message);
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
}
}
}, [eventHandler.eventType]);
const cargarDatos = () => {
loadServicesById(sid)
.then((response: CompanyServiceView) => {
setServiceName(response.name);
setServiceDescription(response.description);
setServiceLength(response.length.toString());
setServiceOriginalPrice(response.originalPrice.toString());
setServiceLimit(response.limit.toString());
setServiceImage(response.image);
setServiceColor(response.color);
setServiceFontColor(response.fontColor);
setServiceFontShadowColor(response.fontShadowColor);
setServicePublished(response.published);
setServiceDiscountType(response.discountType);
setServiceDiscountValue(response.discountValue);
setServiceDiscountName(response.discountName);
})
.catch(() => {})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const formatDiscount = (
value: number | undefined,
type: string | undefined,
name: string | undefined
): string => {
if (value === undefined || value === null || type === undefined || type === null) {
return "";
}
if (type === DiscountType.PERCENTAGE) {
const newPrice = Number(serviceOriginalPrice) * (1 - value / 100);
return `${value}% - ${name} - Precio final: ${formatPrice(newPrice)}`;
} else if (type === DiscountType.FIXED) {
const newPrice = Number(serviceOriginalPrice) - value;
return `${formatPrice(value)} - ${name} - Precio final: ${formatPrice(newPrice)}`;
}
return `${value}`;
};
const handleServiceImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
const file = e.target.files?.[0];
if (!file) {
return;
}
const formData = new FormData();
formData.append("companyId", id);
formData.append("serviceId", sid);
formData.append("sessionUser", SessionInfo.userId); // Añade el ID del usuario
formData.append("file", file);
uploadImage(formData)
.then((res) => {
if (res.success) {
alert.showSuccess("Imagen actualizada");
}
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const getPublishedStatusText = () => {
if (servicePublished === SERVICE_PUBLISHED_STATUS.PUBLISHED) {
return "Público";
} else {
return "Privado";
}
};
const savePublishedStatusHandler = () => {
if (!servicePublished) {
return;
}
const data: SetPublishedStatusParams = {
serviceId: sid,
published: servicePublished,
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
savePublishedStatus(data)
.then(() => {
alert.showSuccess("Estado de publicación actualizado");
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const createDiscountHandler = () => {
if (!SessionInfo.userId) {
return;
}
if (!discountId || discountId === "") {
alert.showError("Debe seleccionar un descuento para aplicar.");
return;
}
const data: ApplyDiscountParams = {
companyId: id,
serviceId: sid,
discountId: discountId,
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
applyDiscount(data)
.then(() => {
alert.showSuccess("Descuento aplicado al servicio");
cargarDatos();
setDialogDiscountVisible(false);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
const deleteDiscountHandler = () => {
if (!SessionInfo.userId) {
return;
}
const data: DeleteDiscountParams = {
companyId: id,
serviceId: sid,
sessionUser: SessionInfo.userId,
};
eventHandler.setEventType(EVENT_TYPES.LOADING);
deleteDiscount(data)
.then(() => {
alert.showSuccess("Descuento aplicado al servicio");
cargarDatos();
setDialogDiscountVisible(false);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
});
};
useEffect(() => {
if (!SessionInfo.userId) {
return;
}
if (!sid || sid === "new") {
return;
}
if (!savePublishedStatusAvailable) {
return;
}
savePublishedStatusHandler();
}, [servicePublished]);
return (
<>
<Location />
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<p style={{ marginTop: "10px" }}>
Desde aquí puedes configurar los datos de un servicio específico.
</p>
<div
style={{
marginTop: "10px",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Switch
checked={servicePublished === SERVICE_PUBLISHED_STATUS.PUBLISHED}
onChange={(checked) => {
setSavePublishedStatusAvailable(true);
setServicePublished(
checked
? SERVICE_PUBLISHED_STATUS.PUBLISHED
: SERVICE_PUBLISHED_STATUS.PRIVATE
);
}}
/>
<span
style={
servicePublished === SERVICE_PUBLISHED_STATUS.PUBLISHED
? { color: "var(--green-darkestX2)" }
: { color: "var(--error)" }
}
>
{getPublishedStatusText()}
</span>
</div>
</div>
<Textbox
name="serviceName"
placeholder="Nombre del servicio"
type="text"
value={serviceName}
width="100%"
limitSize={32}
onChange={(e) => {
setServiceName(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
name="serviceDescription"
placeholder="Descripción del servicio"
type="text"
value={serviceDescription}
width="100%"
limitSize={84}
onChange={(e) => {
setServiceDescription(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<Textbox
name="servicePrice"
placeholder="Precio del servicio"
type="number"
value={serviceOriginalPrice}
width="100%"
onChange={(e) => {
setServiceOriginalPrice(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
{serviceDiscountValue && serviceDiscountValue > 0 ? (
<div
style={{
marginTop: "10px",
color: "var(--green-darkestX2)",
fontWeight: "bold",
}}
>
{formatDiscount(serviceDiscountValue, serviceDiscountType, serviceDiscountName)}
&nbsp;-
<Link
href={""}
onClick={() => {
confirm.show(
"¿Está seguro que desea quitar el descuento para este servicio?",
() => {
deleteDiscountHandler();
},
() => {}
);
}}
>
{" "}
Quitar descuento
</Link>
</div>
) : (
<></>
)}
<Grid2 container>
<Grid2>
<PopoverPicker
placeholder="Fondo"
color={serviceColor}
onChange={(color: string) => {
setServiceColor(color);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
<Grid2 sx={{ width: "40px" }}></Grid2>
<Grid2>
<PopoverPicker
placeholder="Fuente"
color={serviceFontColor}
onChange={(color: string) => {
setServiceFontColor(color);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
<Grid2 sx={{ width: "40px" }}></Grid2>
<Grid2>
<PopoverPicker
placeholder="Sombra"
color={serviceFontShadowColor}
onChange={(color: string) => {
setServiceFontShadowColor(color);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
</Grid2>
</Grid2>
{sid === "new" && (
<MaterialAlert severity="info" sx={{ marginTop: "20px", marginBottom: "20px" }}>
<b>Imagen:</b> Es la imagen representativa de un servicio. Para poder elegirla
primero debe estar creado el servicio.
</MaterialAlert>
)}
<div
style={{
marginTop: "20px",
color: "var(--black)",
fontWeight: "bold",
paddingTop: "5px",
paddingBottom: "10px",
}}
>
Imagen
</div>
<ImagePicker
src={serviceImage}
alt="Logo"
style={{ width: "378px", height: "378px" }}
readOnly={sid === "new"}
onChange={handleServiceImageChange}
mbLimit={5}
imageSizeSrc="/service-image-size.webp"
/>
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Duración del servicio:</b> Este parámetro (en minutos) indica la duración del
turno para este servicio. Por ejemplo, si la duración es 15, el turno durará 15
minutos; si es 30, durará 30 minutos, etc.
</MaterialAlert>
<Textbox
name="serviceLength"
placeholder="Duración del servicio"
type="number"
value={serviceLength}
width="100%"
onChange={(e) => {
setServiceLength(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<MaterialAlert severity="info" sx={{ marginTop: "20px" }}>
<b>Limite del servicio:</b> Este parámetro indica cuántos clientes pueden recibir el
servicio simultáneamente. Si los turnos son personalizados, pon 1. Si son grupales,
por ejemplo, 4 permitirá que 4 clientes reserven al mismo tiempo.
</MaterialAlert>
<Textbox
name="serviceLimit"
placeholder="Limite del servicio"
type="number"
value={serviceLimit}
width="100%"
onChange={(e) => {
setServiceLimit(e.target.value);
eventHandler.setEventType(EVENT_TYPES.NEED_SAVE);
}}
/>
<DialogWindow visible={dialogDiscountVisible} setVisible={setDialogDiscountVisible}>
<>
<DialogContent
style={{
maxWidth: "500px",
minWidth: "300px",
margin: "auto",
}}
>
<>
<TextObjectFilter
placeholder="Descuento"
content={discountsFilterContent}
data={discountsFilterData}
onChange={(id) => {
setDiscountId(id);
}}
/>
</>
</DialogContent>
<DialogActions>
<ButtonMaterial
variant="contained"
color="primary"
onClick={createDiscountHandler}
>
Aplicar Descuento
</ButtonMaterial>
<ButtonMaterial
variant="contained"
color="secondary"
onClick={() => {
setDialogDiscountVisible(false);
}}
>
Cancelar
</ButtonMaterial>
</DialogActions>
</>
</DialogWindow>
<div style={{ marginBottom: "10px" }}>&nbsp;</div>
</>
);
}
@@ -0,0 +1,27 @@
"use client";
import { useParams } from "next/navigation";
import { useRouter } from "next/navigation";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import Addable from "@components/Addable/Addable";
type UrlData = {
id: string;
};
export default function OrgLayout({ children }: { children: React.ReactNode }) {
const urlData = useParams<UrlData>();
const id = urlData.id;
const router = useRouter();
const eventHandler = useEventHandlerStore();
const handleAdd = () => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
router.push("/admin/org/profile/" + id + "/services/new");
};
return (
<>
<Addable onClick={handleAdd}>{children}</Addable>
</>
);
}
@@ -0,0 +1,184 @@
"use client";
import { useParams } from "next/navigation";
import Location from "@components/Location/Location";
import { useLocationStore } from "@store/Location.Store";
import { useEventHandlerStore, EVENT_TYPES } from "@store/EventHandler.Store";
import StorefrontOutlinedIcon from "@mui/icons-material/StorefrontOutlined";
import { useState, useEffect } from "react";
import { loadServicesByCompany } from "./Services.Service";
import { CompanyServiceView } from "@core/Models/Service.model";
import style from "./service.module.css";
import { useNavigation } from "@hooks/goto";
import { useAlert } from "@core/Store/Alert.Store";
import currency from "currency.js";
import { getServiceImage } from "@core/helpers/getServiceImage";
import ShareOutlinedIcon from "@mui/icons-material/ShareOutlined";
import Dialog from "@mui/material/Dialog";
import DialogContent from "@mui/material/DialogContent";
import DialogTitle from "@mui/material/DialogTitle";
import CloseIcon from "@mui/icons-material/Close";
import IconButton from "@mui/material/IconButton";
import {
FacebookShareButton,
WhatsappShareButton,
TwitterShareButton,
EmailShareButton,
TelegramShareButton,
FacebookIcon,
WhatsappIcon as WhatsAppShareIcon,
TwitterIcon,
EmailIcon,
TelegramIcon
} from "react-share";
type UrlData = {
id: string;
};
export default function Org() {
const urlData = useParams<UrlData>();
const id = urlData.id;
const location = useLocationStore();
const eventHandler = useEventHandlerStore();
const alert = useAlert();
const [myServices, setMyServices] = useState<CompanyServiceView[]>([]);
const { goTo } = useNavigation();
const [shareService, setShareService] = useState<CompanyServiceView | null>(null);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
}, []);
useEffect(() => {
location.clear();
location.add({
url: "/admin/org",
title: "Mis organizaciones",
icon: null,
});
location.add({
url: "/admin/org/profile/" + id,
title: "Organizacion",
icon: <StorefrontOutlinedIcon sx={{ mr: 0.5 }} fontSize="inherit" />,
});
location.add({
url: "/admin/org",
title: "Mis servicios",
icon: <StorefrontOutlinedIcon sx={{ fontSize: "32px", mr: "10px" }} />,
});
}, []);
useEffect(() => {
eventHandler.setEventType(EVENT_TYPES.LOADING);
loadServicesByCompany(id)
.then((services) => {
setMyServices(services);
})
.catch((error) => {
alert.showError(error.format());
})
.finally(() => {
eventHandler.setEventType(EVENT_TYPES.NEED_ADD);
});
}, []);
const handleServiceClick = (serviceId: string) => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
goTo("/admin/org/profile/" + id + "/services/" + serviceId);
};
const formatPrice = (price: number) => {
return currency(price, { symbol: "$", separator: ".", decimal: "," }).format();
};
const ServiceItem = ({ data }: { data: CompanyServiceView }) => {
return (
<div
className={style.serviceCard}
style={{
borderLeft: `6px solid ${data.color}`
}}
onClick={() => handleServiceClick(data.id)}
>
<div className={style.imageContainer}>
<img src={getServiceImage(data.image)} alt={data.name} />
</div>
<div className={style.serviceInfo}>
<h1>{data.name}</h1>
<p>{data.description}</p>
</div>
<div className={style.serviceActions}>
<span className={style.servicePrice}>{formatPrice(data.price)}</span>
<IconButton
className={style.shareButton}
sx={{ position: "absolute", top: "15px", right: "15px" }}
onClick={(e) => {
e.stopPropagation();
setShareService(data);
}}
size="small"
title="Compartir servicio"
>
<ShareOutlinedIcon fontSize="small" />
</IconButton>
</div>
</div>
);
};
return (
<>
<Location />
<div className={style.servicesContainer}>
{myServices.map((service) => (
<ServiceItem key={service.id} data={service} />
))}
</div>
<div style={{ marginBottom: "20px" }}>&nbsp;</div>
{shareService && (
<Dialog open={!!shareService} onClose={() => setShareService(null)} maxWidth="sm" fullWidth>
<DialogTitle sx={{ m: 0, p: 2, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
<span style={{ fontWeight: "bold" }}>Compartir Servicio</span>
<IconButton
aria-label="close"
onClick={() => setShareService(null)}
sx={{ color: (theme) => theme.palette.grey[500] }}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent dividers>
<div style={{ textAlign: "center", marginBottom: "30px", fontSize: "16px", color: "var(--black-light)" }}>
Compartí tu servicio <strong>{shareService.name}</strong> para que tus clientes puedan reservarlo directamente.
</div>
<div style={{ display: "flex", justifyContent: "center", flexDirection: "row", alignItems: "center", gap: "20px", paddingBottom: "20px" }}>
<WhatsappShareButton title={"Reserva este servicio: " + shareService.name} separator=":: " url={window.location.origin + "/landing/service/" + shareService.id}>
<WhatsAppShareIcon size={54} round />
</WhatsappShareButton>
<FacebookShareButton title={"Reserva este servicio: " + shareService.name} url={window.location.origin + "/landing/service/" + shareService.id}>
<FacebookIcon size={54} round />
</FacebookShareButton>
<EmailShareButton title={"Reserva este servicio: " + shareService.name} separator=":: " url={window.location.origin + "/landing/service/" + shareService.id}>
<EmailIcon size={54} round />
</EmailShareButton>
<TelegramShareButton title={"Reserva este servicio: " + shareService.name} url={window.location.origin + "/landing/service/" + shareService.id}>
<TelegramIcon size={54} round />
</TelegramShareButton>
<TwitterShareButton title={"Reserva este servicio: " + shareService.name} url={window.location.origin + "/landing/service/" + shareService.id}>
<TwitterIcon size={54} round />
</TwitterShareButton>
</div>
</DialogContent>
</Dialog>
)}
</>
);
}
@@ -0,0 +1,125 @@
.servicesContainer {
display: flex;
flex-direction: column;
gap: 16px;
padding: 0 10px;
margin-top: 10px;
}
.serviceCard {
background: #ffffff;
border-radius: 16px;
padding: 20px;
display: flex;
align-items: center;
gap: 20px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.03), 0 1px 4px rgba(0, 0, 0, 0.02);
border: 1px solid rgba(226, 232, 240, 0.8);
cursor: pointer;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
position: relative;
overflow: hidden;
}
.serviceCard:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.05), 0 3px 8px rgba(0, 0, 0, 0.02);
border-color: rgba(226, 232, 240, 1);
}
.imageContainer {
flex-shrink: 0;
width: 64px;
height: 64px;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.imageContainer img {
width: 100%;
height: 100%;
object-fit: cover;
}
.serviceInfo {
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
}
.serviceInfo h1 {
font-size: 18px;
font-weight: 700;
color: #1e293b;
margin: 0 0 4px 0;
}
.serviceInfo p {
font-size: 14px;
color: #64748b;
margin: 0;
line-height: 1.4;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.serviceActions {
display: flex;
flex-direction: column;
align-items: flex-end;
justify-content: flex-end; /* changed from space-between */
min-width: 100px;
}
.servicePrice {
font-size: 18px;
font-weight: 800;
color: #1e293b;
margin-bottom: 0; /* removed bottom margin */
}
.shareButton {
color: #94a3b8;
transition: color 0.2s ease;
position: absolute;
top: 15px;
right: 15px;
}
.shareButton:hover {
color: var(--wine-dark);
}
@media (max-width: 768px) {
.serviceCard {
flex-direction: column;
align-items: flex-start;
gap: 15px;
}
.serviceActions {
width: 100%;
flex-direction: row;
align-items: center;
justify-content: flex-start; /* Price on the left */
margin-top: 10px;
padding-top: 15px;
border-top: 1px solid #f1f5f9;
}
.servicePrice {
margin-bottom: 0;
}
}
/* Alternating Pastel Colors for Cards */
.serviceCard:nth-child(6n + 1) { background-color: #f0f9ff; }
.serviceCard:nth-child(6n + 2) { background-color: #f5f3ff; }
.serviceCard:nth-child(6n + 3) { background-color: #f0fdf4; }
.serviceCard:nth-child(6n + 4) { background-color: #fff7ed; }
.serviceCard:nth-child(6n + 5) { background-color: #fdf2f8; }
.serviceCard:nth-child(6n + 6) { background-color: #fefce8; }
@@ -0,0 +1,62 @@
import ApiRequest from "@services/Api.Service";
import * as Yup from "yup";
import { ApiError } from "@core/Models/Server.Error.model";
import {
DeleteTemplateParams,
FindTemplatesParams,
GetDefaultTemplateParams,
ITemplate,
PaginateTemplatesParams,
PaginateTemplatesResults,
SaveTemplateParams,
} from "@core/Models/Templates.model";
const schemaTemplateSave = Yup.object().shape({
templateId: Yup.string().optional(),
companyId: Yup.string().required("No es posible deteriminar la organización."),
name: Yup.string().required("Debe introducir el nombre del descuento."),
code: Yup.string().required("Debe introducir el código del descuento."),
type: Yup.string().required("Debe seleccionar el tipo de descuento."),
template: Yup.string().required("Debe definir una plantilla."),
sessionUser: Yup.string().required("Para crear un descuento debe iniciar sesión."),
});
export const saveTemplate = async (data: SaveTemplateParams): Promise<ITemplate> => {
let yupErrors: Yup.ValidationError = new Yup.ValidationError("");
const validateData = async () => {
try {
await schemaTemplateSave.validate(data, { abortEarly: true });
return true;
} catch (error) {
if (error instanceof Yup.ValidationError) {
yupErrors = error as Yup.ValidationError;
}
return false;
}
};
if ((await validateData()) == false) {
throw new ApiError(400, yupErrors.message);
}
return ApiRequest.post<ITemplate>("templates/save", data);
};
export const findTemplatesByOrganization = async (
data: PaginateTemplatesParams
): Promise<PaginateTemplatesResults> => {
return ApiRequest.post<PaginateTemplatesResults>("templates/paginate", data);
};
export const getTemplate = async (data: FindTemplatesParams): Promise<ITemplate> => {
return ApiRequest.post<ITemplate>("templates/get", data);
};
export const getDefaultTemplate = async (data: GetDefaultTemplateParams): Promise<string> => {
return ApiRequest.post<string>("templates/get-default", data);
};
export const deleteTemplate = async (data: DeleteTemplateParams): Promise<void> => {
return ApiRequest.post<void>("templates/delete", data);
};
@@ -0,0 +1,28 @@
"use client";
import { useParams } from "next/navigation";
import { EVENT_TYPES, useEventHandlerStore } from "@store/EventHandler.Store";
import { useNavigation } from "@hooks/goto";
import Addable from "@components/Addable/Addable";
type UrlData = {
id: string;
};
export default function ClientLayout({ children }: { children: React.ReactNode }) {
const urlData = useParams<UrlData>();
const id = urlData.id;
const eventHandler = useEventHandlerStore();
const { goTo } = useNavigation();
const handleAdd = () => {
eventHandler.setEventType(EVENT_TYPES.SLEEP);
goTo("/admin/org/profile/" + id + "/templates/new/new");
};
return (
<>
<Addable onClick={handleAdd}>{children}</Addable>
</>
);
}
@@ -0,0 +1,11 @@
"use client";
import Saveable from "@components/Saveable/Saveable";
export default function CreateDiscountsLayout({ children }: { children: React.ReactNode }) {
return (
<>
<Saveable>{children}</Saveable>
</>
);
}

Some files were not shown because too many files have changed in this diff Show More