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
@@ -0,0 +1,4 @@
export type ApiVoidResult = {
success: boolean;
message: string;
};
+206
View File
@@ -0,0 +1,206 @@
import { DiscountType } from "./Discounts.model";
export type CreateAppointmentParams = {
companyId: string;
serviceId: string;
employeeId: string;
userId?: string; // for user reservations
clientId?: string; // for collaborator reservations
repeatId?: string; //Para saber si tiene un descuento.
start: string;
length?: number;
price?: number;
comments?: string;
validation?: boolean;
notification?: boolean;
sessionUser: string;
};
export type UpdateAppointmentParams = {
id: string;
start: string;
length: number;
price: number;
comments: string;
validation?: boolean;
notification?: boolean;
sessionUser: string;
};
export enum APPOINTMENT_NOTIFICATION_TYPE {
"CREATION" = "creation",
"REMINDER" = "reminder",
}
export type SendAppointmentNotificationParams = {
appointmentId: string;
sessionUser: string;
type: APPOINTMENT_NOTIFICATION_TYPE;
};
export type DeleteAppointmentParams = {
id: string;
validation?: boolean;
sessionUser: string;
};
export type FindAppointmentSchedulesParams = {
companyId: string;
serviceId: string;
employeeId: string;
dateDay: string | Date;
};
export type GetAvailableDatesParams = {
companyId: string;
serviceId: string;
employeeId: string;
length: number;
sessionUser: string;
};
export type GetAvailableDatesResult = {
availableDates: Date[];
inRangeDates: Date[];
};
export interface GetAppointmentEventParams {
appointmentId: string;
sessionUser: string;
}
export interface IAppointment {
id?: string;
companyId: string;
serviceId: string;
employeeId: string;
clientId: string;
discountId: string;
discountType: DiscountType;
discountValue: number;
discountName: string;
discountCode: string;
datetime: Date;
length: number;
price: number;
present: boolean;
payment: boolean;
comments: string;
creationDate: Date;
}
export type FindAppointmentsParams = {
_id?: string;
companyId?: string;
serviceId?: string;
employeeId?: string;
clientId?: string;
start?: string;
dateDay?: string;
};
export type FindAppointmentsByUserParams = {
sessionUser: string;
};
export type FindAppointmentsByUserPaginatedParams = FindAppointmentsByUserParams & {
page: number;
limit: number;
};
export type FindAppointmentsByCollaboratorParams = {
sessionUser: string;
};
export type FindAppointmentsByCollaboratorPaginatedParams = FindAppointmentsByCollaboratorParams & {
page: number;
limit: number;
};
export interface AppointmentEvent {
appointmentId: string;
startTime: number;
endTime: number;
title: string;
clientId: string;
serviceId: string;
employeeId: string;
avatar: string;
color: string;
repeatId: string;
discountId: string;
discountType: DiscountType;
discountValue: number;
discountName: string;
discountCode: string;
}
export interface AppointmentAdminByDateView {
events: AppointmentEvent[];
from: number;
to: number;
}
export interface AppointmentEventByClient {
id: string;
companyId: string;
serviceId: string;
serviceName: string;
serviceDescription: string;
collaboratorId: string;
collaboratorName: string;
collaboratorAvatar: string;
appointmentDate: string;
startTime: number;
endTime: number;
title: string;
clientId: string;
clientName: string;
avatar: string;
color: string;
comments: string;
price: number;
length: number;
payment: boolean;
discountId: string;
discountType: DiscountType;
discountValue: number;
discountName: string;
discountCode: string;
clientCtaCteBalance: number;
}
export interface AppointmentAdminByClientView {
events: AppointmentEventByClient[];
}
export interface PaginatedAppointmentEventByClientResult {
data: AppointmentEventByClient[];
page: number;
pages: number;
}
export type ApplyAppointmentDiscountParams = {
companyId: string;
appointmentId: string;
discountId: string;
sessionUser: string;
};
export type DeleteAppointmentDiscountParams = {
appointmentId: string;
companyId: string;
sessionUser: string;
};
export type ChangeServiceParams = {
id: string;
newServiceId: string;
newEmployeeId: string;
sessionUser: string;
};
+132
View File
@@ -0,0 +1,132 @@
import {
CLIENT_ACCOUNT_MOVEMENT_TYPES,
CLIENT_ACCOUNT_PAYMENT_METHODS,
} from "./ClientAccountMovements.model";
import { PAYMENT_METHODS_CLIENT_ACCOUNT, PAYMENT_PROCCESS_DATE } from "./Payments.model";
export type FindCashFlowParams = {
id?: string;
companyId: string;
paymentId?: string;
clientId?: string;
clientAccountMovementId?: string;
appointmentId?: string;
employeeId?: string;
type?: CLIENT_ACCOUNT_MOVEMENT_TYPES;
paymentMethod?: CLIENT_ACCOUNT_PAYMENT_METHODS | PAYMENT_METHODS_CLIENT_ACCOUNT;
startDate?: Date;
endDate?: Date;
year?: number;
month?: number;
reference?: string;
sessionUser: string;
};
export type PaginateCashFlowParams = FindCashFlowParams & {
page: number;
limit: number;
};
export type PaginateCashFlowResults = {
data: ICashFlow[];
balance: number;
page: number;
pages: number;
};
export type CreateCashFlowParams = {
companyId: string;
paymentId?: string;
clientId?: string;
clientAccountMovenemtId?: string;
appointmentId?: string;
employeeId?: string;
amount: number;
description: string;
reference: string;
year: number;
month: number;
type: CLIENT_ACCOUNT_MOVEMENT_TYPES;
paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS | PAYMENT_METHODS_CLIENT_ACCOUNT;
proccessDate: PAYMENT_PROCCESS_DATE;
sessionUser: string;
};
export type DeleteMovementByPaymentParams = {
companyId: string;
paymentId: string;
sessionUser: string;
};
export type DeleteMovementParams = {
id: string;
companyId: string;
sessionUser: string;
};
export type DeleteByClientAccountMovementParams = {
companyId: string;
clientAccountMovementId: string;
sessionUser: string;
};
export interface ICashFlow {
id: string;
companyId: string;
paymentId?: string;
clientId?: string;
clientAccountMovementId?: string;
appointmentId?: string;
employeeId?: string;
amount: number;
description: string;
reference: string;
year: number;
month: number;
type: CLIENT_ACCOUNT_MOVEMENT_TYPES;
paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS | PAYMENT_METHODS_CLIENT_ACCOUNT;
createdAt: Date;
}
export type FindCashFlowBalanceParams = {
companyId: string;
year: number;
month: number;
sessionUser: string;
};
export type CreateCashFlowBalanceParams = {
companyId: string;
amount: number;
year: number;
month: number;
updatedAt: Date;
};
export type CalculateCashFlowBalanceParams = {
companyId: string;
year: number;
month: number;
sessionUser: string;
};
export interface ICashFlowBalance {
id?: string;
companyId: string;
amount: number;
year: number;
month: number;
updatedAt: Date;
}
export interface CashFlowHistoryItem {
amount: number;
month: number;
year: number;
}
export interface CashFlowHistoryView {
companyId: string;
data: CashFlowHistoryItem[];
total: number;
}
+74
View File
@@ -0,0 +1,74 @@
export interface ICategory {
id: number;
name: string;
description: string;
image?: string;
color?: string;
fontColor?: string;
fontShadowColor?: string;
active?: boolean;
icon?: string;
}
export const NO_CATEGORY: number = 0;
const categories: ICategory[] = [
{
id: 100,
name: "Bienestar y Salud",
description: "Servicios relacionados con la mejora de la salud y el bienestar personal.",
image: "",
color: "#ff6f61", // Color representativo
fontColor: "#ffffff",
fontShadowColor: "#333333",
active: true,
icon: "health-icon",
},
{
id: 200,
name: "Servicios Educativos",
description: "Categoría que abarca servicios de educación y formación.",
image: "",
color: "#42a5f5",
fontColor: "#ffffff",
fontShadowColor: "#222222",
active: true,
icon: "education-icon",
},
{
id: 300,
name: "Servicios Contables",
description: "Servicios para la gestión contable y financiera de empresas o individuos.",
image: "",
color: "#8e44ad",
fontColor: "#ffffff",
fontShadowColor: "#000000",
active: true,
icon: "accounting-icon",
},
{
id: 400,
name: "Servicios de Construcción y Mantenimiento",
description:
"Servicios especializados en construcción, reparación y mantenimiento de infraestructuras.",
image: "",
color: "#f39c12",
fontColor: "#000000",
fontShadowColor: "#ffffff",
active: true,
icon: "construction-icon",
},
{
id: 50000,
name: "Otros",
description: "Otros servicios",
image: "",
color: "#95a5a6",
fontColor: "#000000",
fontShadowColor: "#ffffff",
active: true,
icon: "other-icon",
},
];
export default categories;
@@ -0,0 +1,79 @@
import { PAYMENT_METHODS } from "./Payments.model";
export type FindClientAccountMovementsParams = {
id?: string;
clientId: string;
type?: CLIENT_ACCOUNT_MOVEMENT_TYPES;
status?: CLIENT_ACCOUNT_MOVEMENT_STATUS;
reference?: string;
startDate?: Date;
endDate?: Date;
sessionUser: string;
method?: PAYMENT_METHODS;
};
export type PaginateClientAccountMovementsParams =
FindClientAccountMovementsParams & {
page: number;
limit: number;
};
export type PaginateClientAccountMovementsResults = {
data: IClientAccountMovement[];
page: number;
pages: number;
};
export type CreateClientAccountMovementParams = {
clientId: string;
amount: number;
description?: string;
type: CLIENT_ACCOUNT_MOVEMENT_TYPES;
reference?: string;
status: CLIENT_ACCOUNT_MOVEMENT_STATUS;
paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS;
notification?: boolean;
sessionUser: string;
};
export type DeleteClientAccountMovementParams = {
movementId: string;
sessionUser: string;
};
export type CalculateClientAccountBalanceParams = {
clientId: string;
sessionUser: string;
};
export enum CLIENT_ACCOUNT_MOVEMENT_TYPES {
DEPOSIT = "deposit",
WITHDRAWAL = "withdrawal",
}
export enum CLIENT_ACCOUNT_MOVEMENT_STATUS {
PENDING = "pending",
APPROVED = "approved",
REJECTED = "rejected",
}
export enum CLIENT_ACCOUNT_PAYMENT_METHODS {
CASH = "cash",
BANK_TRANSFER = "bank_transfer",
CREDIT_CARD = "credit_card",
DEBIT_CARD = "debit_card",
OTHER = "other",
}
export interface IClientAccountMovement {
id: string;
clientId: string;
amount: number;
description: string;
type: CLIENT_ACCOUNT_MOVEMENT_TYPES;
reference: string;
status: CLIENT_ACCOUNT_MOVEMENT_STATUS;
paymentMethod: CLIENT_ACCOUNT_PAYMENT_METHODS;
createdAt: Date;
updatedAt: Date;
}
@@ -0,0 +1,56 @@
import { IClientAccountMovement } from "./ClientAccountMovements.model";
export type FindClientAccountsParams = {
clientId: string;
sessionUser: string;
};
export type CreateClientAccountParams = {
clientId: string;
amount: number;
sessionUser: string;
};
export type DeleteClientAccountParams = {
clientId: string;
sessionUser: string;
};
export type ClientAccountReportItem = {
date: Date;
description: string;
credit: number;
debit: number;
balance: number;
};
export type ClientAccountReport = {
organizacionName: string;
organizationId: string;
clientId: string;
clientName: string;
balance: number;
dateFrom: Date;
dateTo: Date;
data: ClientAccountReportItem[];
};
export type GetClientAccountReportParams = {
clientId: string;
dateFrom: Date;
dateTo: Date;
sessionUser: string;
};
export interface ClientAccountView {
id: string;
clientId: string;
amount: number;
lastMovements: IClientAccountMovement[];
}
export interface IClientAccount {
id?: string;
clientId: string;
amount: number;
}
+81
View File
@@ -0,0 +1,81 @@
export type FindClientsParams = {
_id?: string;
companyId?: string;
email?: string;
firstName?: string;
lastName?: string;
userId?: string;
status?: boolean;
};
export type FindClientByIdParams = {
id: string;
companyId: string;
sessionUser: string;
};
export type PaginateOrganizationClientsParams = FindClientsParams & {
page: number;
limit: number;
};
export type OrganizationClientsResults = {
data: OrganizationClientView[];
page: number;
pages: number;
};
export type CreateCustomClientParams = {
companyId: string;
sessionUser: string;
firstName?: string;
lastName?: string;
phoneNumber?: string;
phoneCountryCode?: string;
phoneAreaCode?: string;
email?: string;
banned?: boolean;
};
export type UpdateClientParams = CreateCustomClientParams & {
id: string;
};
export type ClientMergeParams = {
fromClientId: string;
toClientId: string;
sessionUser: string;
};
export type DeleteClientParams = {
clientId: string;
sessionUser: string;
};
export interface OrganizationClientView {
id: string;
firstName: string;
lastName: string;
fullName: string;
phoneNumber: string;
phoneCountryCode: string;
phoneAreaCode: string;
email: string;
avatar: string;
banned?: boolean;
}
export interface IClient {
id?: string;
companyId: string;
userId: string;
fromClientId: string;
status: boolean;
firstName: string;
lastName: string;
phoneNumber: string;
phoneCountryCode: string;
phoneAreaCode: string;
email: string;
banned?: boolean;
}
+145
View File
@@ -0,0 +1,145 @@
export type FindEmployeesParams = {
_id?: string;
name?: string;
companyId?: string;
userId?: string;
};
export type FindEmployeesByServiceParams = {
serviceId?: string;
};
export enum EmployeeRoles {
ADMIN = "admin",
OWNER = "owner",
EMPLOYEE = "employee",
}
export interface IEmployee {
id?: string;
companyId: string;
userId: string;
roles?: EmployeeRoles[];
guestOk?: boolean;
hostOk?: boolean;
}
export interface CompanyEmployeesView {
id: string;
companyId: string;
userId: string;
firstName: string;
lastName: string;
fullName: string;
avatar: string;
email: string;
roles: EmployeeRoles[];
guestOk: boolean;
hostOk: boolean;
fullOk: boolean;
calendarColor: string;
}
export type CreateEmployeeParams = {
companyId: string;
userId: string;
roles?: EmployeeRoles[];
sessionUser: string;
};
export type UpdateEmployeeParams = {
id: string;
calendarColor?: string;
limit?: number;
sessionUser: string;
};
export type UpdateEmployeeRolesParams = {
employeeId: string;
companyId: string;
roles: EmployeeRoles[];
sessionUser: string;
};
export type ValidateEmployeeParams = {
employeeId: string;
sessionUser: string;
};
export type FindEmployeesByIdParams = {
id?: string;
};
export type FindCollaboratorServicesParams = {
companyId: string;
employeeId: string;
sessionUser: string;
};
export type FindEmployeesServicesParams = {
_id?: string;
companyId?: string;
employeeId?: string;
serviceId?: string;
};
export type UpdateCollaboratorServicesParams = {
companyId: string;
employeeId: string;
services: ServiceStatusItem[];
sessionUser: string;
};
export interface CollaboratorServiceItems {
id: string;
name: string;
description: string;
length: number;
price: number;
limit: number;
active: boolean;
}
export interface CollaboratorServiceView {
companyId: string;
employeeId: string;
fullName: string;
avatar: string;
services: CollaboratorServiceItems[];
}
export interface ServiceStatusItem {
serviceId: string;
active: boolean;
}
export type ScheduleItem = {
from: string;
to: string;
disabled: boolean;
};
export type FindSchedulesParams = {
_id?: string;
companyId?: string;
employeeId?: string;
weekDay?: number;
};
export interface CollaboratorSchedulesView {
companyId: string;
employeeId: string;
scheduleId: string;
weekDay: number;
fullName: string;
avatar: string;
schedules: ScheduleItem[];
}
export type UpdateScheduleParams = {
companyId: string;
employeeId: string;
schedules: Array<ScheduleItem>;
weekDay: number;
sessionUser: string;
};
+200
View File
@@ -0,0 +1,200 @@
import { PublicServicesResults } from "./Service.model";
export interface MyOranizationsView {
id: string;
name: string;
slug: string;
description: string;
categoryId: number;
ownerId: string;
employeeId: string;
isOwner: boolean;
isAdmin: boolean;
hostOk: boolean;
guestOk: boolean;
fullOk: boolean;
headerFile: string;
headerColor: string;
headerFontColor: string;
headerFontShadowColor: string;
iconFile: string;
logoFile: string;
phoneAreaCode: string;
phoneNumber: string;
heatMapFraction: number;
cancellationTime: number;
appointmentTime: number;
street: string;
streetNumber: string;
builingFloor: string;
buildingApartament: string;
block: string;
city: string;
state: string;
country: string;
zipCode: string;
latitude: number;
longitude: number;
published: COMPANY_PUBLISHED_STATUS;
appointmentAlert: string;
templateWapNotifId: string;
templateWapAltaId: string;
templateEmailNotifId: string;
templateEmailAltaId: string;
onboardingStep?: number;
onboardingCompleted?: boolean;
}
export type CreateCompanyParams = {
name: string;
description: string;
categoryId: number;
ownerId: string;
onboardingStep?: number;
};
export type UpdateCompanyParams = {
id: string;
name?: string;
description?: string;
categoryId?: number;
headerFile?: string;
iconFile?: string;
logoFile?: string;
headerColor?: string;
headerFontColor?: string;
headerFontShadowColor?: string;
street?: string;
streetNumber?: string;
builingFloor?: string;
buildingApartament?: string;
block?: string;
city?: string;
state?: string;
country?: string;
zipCode?: string;
latitude?: number;
longitude?: number;
phoneCountryCode?: string;
phoneAreaCode?: string;
phoneNumber?: string;
heatMapFraction?: number;
appointmentTime?: number;
cancellationTime?: number;
appointmentAlert?: string;
sessionUser: string;
templateWapNotifId?: string;
templateWapAltaId?: string;
templateEmailNotifId?: string;
templateEmailAltaId?: string;
onboardingStep?: number;
onboardingCompleted?: boolean;
};
export type DeleteCompanyParams = {
companyId: string;
sessionUser: string;
};
export enum COMPANY_PUBLISHED_STATUS {
PUBLISHED = "published",
PRIVATE = "private",
}
export type SetOrganizationPublishedStatusParams = {
companyId: string;
published: COMPANY_PUBLISHED_STATUS;
sessionUser: string;
};
export interface OranizationsViewByIdParams {
id: string;
sessionUser: string;
}
export interface ICompany {
id?: string;
name: string;
slug: string;
description: string;
categoryId: number;
ownerId: string;
headerFile?: string;
headerColor?: string;
headerFontColor?: string;
headerFontShadowColor?: string;
iconFile?: string;
logoFile?: string;
street?: string;
streetNumber?: string;
builingFloor?: string;
buildingApartament?: string;
block?: string;
city?: string;
state?: string;
country?: string;
zipCode?: string;
latitude?: number;
longitude?: number;
phoneCountryCode?: string;
phoneAreaCode?: string;
phoneNumber?: string;
heatMapFraction?: number;
appointmentTime?: number;
cancellationTime?: number;
published?: COMPANY_PUBLISHED_STATUS;
appointmentAlert?: string;
templateWapNotifId?: string;
templateWapAltaId?: string;
templateEmailNotifId?: string;
templateEmailAltaId?: string;
onboardingStep?: number;
onboardingCompleted?: boolean;
fixedPostIds?: Array<string>;
banned?: boolean;
}
export type PublicOrganizationViewParams = {
companyId: string;
};
export type PublicOrganizationView = {
id: string;
headerFile: string;
headerColor: string;
headerFontColor: string;
headerFontShadowColor: string;
iconFile: string;
logoFile: string;
categoryId: number;
name: string;
slug: string;
description: string;
services: PublicServicesResults;
employees: PublicServicesResults;
address: string;
phone: string;
latitude: number;
longitude: number;
appointmentAlert: string;
banned?: boolean;
};
export type SetNotificationAutoParams = {
companyId: string;
status?: boolean;
sessionUser: string;
};
export type FixCompanyPostParams = {
companyId: string;
postId: string;
sessionUser: string;
};
export type MoveCompanyFixedPostParams = {
companyId: string;
postId: string;
moveLength: number;
sessionUser: string;
};
+53
View File
@@ -0,0 +1,53 @@
export enum DiscountType {
FIXED = "fixed",
PERCENTAGE = "percentage",
}
export interface IDiscount {
id: string;
_id: string;
companyId: string;
name: string;
code: string;
type: DiscountType;
value: number;
createdAt: Date;
}
export type FindDiscountsParams = {
id?: string;
companyId: string;
code?: string;
type?: DiscountType;
sessionUser: string;
startDate?: Date;
endDate?: Date;
startValue?: number;
endValue?: number;
};
export type PaginateDiscountsParams = FindDiscountsParams & {
page: number;
limit: number;
};
export type PaginateDiscountsResults = {
data: IDiscount[];
page: number;
pages: number;
};
export type CreateDiscountParams = {
companyId: string;
name: string;
code: string;
type: DiscountType;
value: number;
sessionUser: string;
};
export type DeleteDiscountParams = {
id: string;
companyId: string;
sessionUser: string;
};
@@ -0,0 +1,64 @@
export interface IDiscountEmail {
id?: string;
_id?: string;
companyId: string;
discountId: string;
email: string;
startDate: Date;
endDate: Date;
createdAt: Date;
}
export type DiscountEmailView = {
id: string;
companyId: string;
discountId: string;
discountName: string;
discountType: string;
discountValue: number;
discountCode: string;
email: string;
startDate: Date;
endDate: Date;
};
export type FindDiscountsEmailParams = {
id?: string;
companyId: string;
discountId?: string;
email?: string;
sessionUser: string;
searchDate?: Date;
};
export type PaginateDiscountsEmailParams = FindDiscountsEmailParams & {
page: number;
limit: number;
};
export type PaginateDiscountsEmailResults = {
data: IDiscountEmail[];
page: number;
pages: number;
};
export type PaginateDiscountsEmailViewResults = {
data: DiscountEmailView[];
page: number;
pages: number;
};
export type CreateDiscountEmailParams = {
companyId: string;
discountId: string;
email: string;
startDate: Date;
endDate: Date;
sessionUser: string;
};
export type DeleteDiscountEmailParams = {
id: string;
companyId: string;
sessionUser: string;
};
+16
View File
@@ -0,0 +1,16 @@
export type UploadPostFileParams = {
companyId: string;
sessionUser: string;
token: string; //Session User token. Para verificar que sea un usuario loguedo.
file: File;
};
export type UploadedFileInfo = {
originalName: string;
objectName: string;
};
export type UploadFileResponse = {
message: string;
files: UploadedFileInfo[];
};
+26
View File
@@ -0,0 +1,26 @@
export type FindProvinciasParams = {
_id?: string;
name?: string;
code?: number;
};
export type FindPartidosParams = {
_id?: string;
name?: string;
code?: number;
provincia: string;
};
export type FindLocalidadesParams = {
_id?: string;
name?: string;
code?: number;
partido: string;
provincia: string;
};
export type PaginateFilterBlockParams = {
name?: string;
page: number;
limit: number;
};
+148
View File
@@ -0,0 +1,148 @@
/**
* Convesations
*/
export enum MESSAGE_CONVERSATION_TYPES {
"USER_TO_USER" = "user-to-user",
"USER_TO_ORG" = "user-to-org",
}
export type FindMessageConversationsParams = {
_id?: string;
userIdFrom?: string;
userIdTo?: string;
companyId?: string;
sessionUser: string;
};
export type PaginateMessageConversationsParams = {
query?: string;
userIdFrom?: string;
userIdTo?: string;
companyId?: string;
page: number;
limit: number;
sessionUser: string;
};
export type PaginateMessageConversationsResults = {
data: IMessageConversation[];
page: number;
pages: number;
};
export type FindConversationsResults = {
data: FindConversationItem[];
page: number;
pages: number;
};
export type CreateMessageConversationParams = {
userIdFrom?: string;
userIdTo?: string;
companyId?: string;
type: MESSAGE_CONVERSATION_TYPES;
sessionUser: string;
};
export type DeleteMessageConversationParams = {
id: string;
sessionUser: string;
};
export interface IMessageConversation {
id?: string;
userIdFrom?: string; //Indica quien envía el mensaje
userIdTo?: string; // Indica a que usuario va dirigido el mensaje.
// Cuando el mensaje va dirigido a una organizacion,
// habrá un colaborador de la organizacion que de una respuesta
// y ese id de usuario se almacenara en userIdTo.
companyId?: string; //Indica que el mensaje va dirigido a una organizacion.
type: MESSAGE_CONVERSATION_TYPES;
newMessages: number; //Indica si hay nuevos mensajes.
}
export interface FindConversationItem {
conversation: IMessageConversation;
name: string;
description: string;
image: string;
orderWeight: number;
}
export type PaginateFindContactsParams = {
query?: string;
page: number;
limit: number;
sessionUser: string;
};
export type PaginateFindContactsResults = {
data: FindConversationItem[];
page: number;
pages: number;
};
export type MarkConversationAsReadParams = {
conversationId: string;
sessionUser: string;
};
/**
* Mensages
*/
export type FindMessageParams = {
id?: string;
conversationId?: string;
messageDate?: Date;
readed?: boolean;
sessionUser: string;
};
export type PaginateMessageParams = {
query?: string;
conversationId?: string;
dateFrom?: Date;
dateTo?: Date;
readed?: boolean;
page: number;
limit: number;
sessionUser: string;
};
export type PaginateMessageResults = {
data: IMessage[];
page: number;
pages: number;
};
export type SendMessageParams = Omit<CreateMessageConversationParams, "type"> & {
conversationId?: string;
type?: MESSAGE_CONVERSATION_TYPES;
message: string;
};
export type CreateMessageParams = {
conversationId: string;
message: string;
sessionUser: string;
};
export type DeleteMessageParams = {
id: string;
sessionUser: string;
};
export interface IMessage {
id?: string;
conversationId?: string;
messageDate: Date; //Fecha de envío del mensaje
readed: boolean; //Indica si el mensaje fue leido por el receptor.
message: string; //Contenido del mensaje.
userId: string; //Propietario del mensaje.
}
export interface ISendMessageResult {
message: IMessage;
conversation: IMessageConversation;
}
+24
View File
@@ -0,0 +1,24 @@
export interface IMetrics {
id?: string;
userId: string;
organizationsCount: number;
employeesCount: number;
servicesCount: number;
appointmentsCount: number;
clientsCount: number;
repeatsCount: number;
month: number;
year: number;
}
export const DefaultMetricsModel: IMetrics = {
userId: "",
organizationsCount: 0,
employeesCount: 0,
servicesCount: 0,
appointmentsCount: 0,
clientsCount: 0,
repeatsCount: 0,
month: 0,
year: 0,
};
+174
View File
@@ -0,0 +1,174 @@
import {
CLIENT_ACCOUNT_MOVEMENT_STATUS,
CLIENT_ACCOUNT_PAYMENT_METHODS,
} from "./ClientAccountMovements.model";
export enum PAYMENT_METHODS_CLIENT_ACCOUNT {
CLIENT_ACCOUNT = "ACCOUNT",
}
export type PAYMENT_METHODS = CLIENT_ACCOUNT_PAYMENT_METHODS | PAYMENT_METHODS_CLIENT_ACCOUNT;
export type FindPaymentsParams = {
id?: string;
clientId?: string;
clientIds?: string[];
companyId?: string;
appointmentId?: string;
employeeId?: string;
serviceId?: string;
status?: CLIENT_ACCOUNT_MOVEMENT_STATUS;
method?: PAYMENT_METHODS;
startDate?: Date;
endDate?: Date;
proccessDate?: PAYMENT_PROCCESS_DATE;
sessionUser: string;
};
export type PaginatePaymentsParams = FindPaymentsParams & {
page: number;
limit: number;
};
export type PaginatePaymentsResults = {
data: IPayment[];
page: number;
pages: number;
};
export enum PAYMENT_PROCCESS_DATE {
NOW = "now",
APPOINTMENT = "appointment",
}
export type CreatePaymentParams = {
amount: number;
method: PAYMENT_METHODS;
status: CLIENT_ACCOUNT_MOVEMENT_STATUS;
appointmentId: string;
description?: string;
proccessDate: PAYMENT_PROCCESS_DATE;
sessionUser: string;
};
export type CreatePaymentExtraParams = CreatePaymentParams & {
clientId: string;
companyId: string;
employeeId: string;
serviceId: string;
};
export type DeletePaymentParams = {
id: string;
sessionUser: string;
};
export type GetAppointmentPaymentsParams = {
appointmentId: string;
sessionUser: string;
};
export type MarkAsPayedParams = {
appointmentId: string;
method: PAYMENT_METHODS;
sessionUser: string;
};
export interface PaginatePaymentsView {
id: string;
appointmentId: string;
appointmentDate: Date;
clientId: string;
clientName: string;
employeeId: string;
employeeName: string;
serviceId: string;
serviceName: string;
price: number;
status: CLIENT_ACCOUNT_MOVEMENT_STATUS;
method: PAYMENT_METHODS;
description: string;
createdAt: Date;
}
export type PaginateNotPayedAppointmentsParams = FindPaymentsParams & {
page: number;
limit: number;
};
export type PaginateNotPayedAppointmentsResult = {
data: PaginatePaymentsView[];
page: number;
pages: number;
};
export type PaginatePaymentsViewParams = FindPaymentsParams & {
page: number;
limit: number;
};
export type PaginatePaymentsViewResults = {
data: PaginatePaymentsView[];
page: number;
pages: number;
};
export interface IPayment {
id: string;
_id: string;
amount: number;
status: CLIENT_ACCOUNT_MOVEMENT_STATUS;
method: PAYMENT_METHODS;
clientId: string;
companyId: string;
employeeId: string;
serviceId: string;
appointmentId: string;
description: string;
createdAt: Date;
updatedAt: Date;
}
export const paymentMethods = [
{
id: CLIENT_ACCOUNT_PAYMENT_METHODS.CASH,
name: "Efectivo",
description: "Efectivo",
},
{
id: PAYMENT_METHODS_CLIENT_ACCOUNT.CLIENT_ACCOUNT,
name: "Cuenta Corriente",
description: "Dinero en cuenta",
},
{
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",
},
];
export type NotPayedReportResult = {
items: PaginatePaymentsView[];
totalAmount: number;
};
export type PaymentsReportResult = {
items: PaginatePaymentsView[];
totalAmount: number;
};
+28
View File
@@ -0,0 +1,28 @@
export interface IPlan {
id?: string;
name: string;
description: string;
features: string[];
code: string;
price: number;
annualPrice: number;
limitOrganizations: number;
limitEmployees: number;
limitServices: number;
limitAppointments: number; //Cantidad de turnos por día.
limitClients: number;
limitRepeats: number;
mailNotifications: boolean;
smsNotifications: boolean;
wapNotifications: boolean;
payments: boolean;
bot: boolean;
active: boolean;
dateLimit: boolean; //Indica se el plan tiene un limite de fecha.
//Por ejemplo, el plan gratuito no tiene un limite de fecha.
mpPlanId?: string;
discount3Months?: number;
discount6Months?: number;
discount12Months?: number;
featured?: boolean;
}
+46
View File
@@ -0,0 +1,46 @@
export interface IPost {
id?: string;
_id?: string;
companyId: string;
content: string;
fixed: boolean;
fixedOrder: number;
createdAt: Date;
}
export type FindPostsParams = {
id?: string;
companyId: string;
fixed?: boolean;
sessionUser?: string;
};
export type GetPostParams = {
id: string;
};
export type PaginatePostsParams = FindPostsParams & {
page: number;
limit: number;
};
export type PaginatePostsResults = {
data: IPost[];
page: number;
pages: number;
authorizedUsers: string[];
};
export type SavePostParams = {
postId?: string;
companyId: string;
content: string;
fixed?: boolean;
fixedOrder?: number;
sessionUser: string;
};
export type DeletePostParams = {
id: string;
companyId: string;
sessionUser: string;
};
+88
View File
@@ -0,0 +1,88 @@
import { DiscountType } from "./Discounts.model";
export type FindRepeatsParams = {
_id?: string;
companyId?: string;
employeeId?: string;
serviceId?: string;
clientId?: string;
weekDay?: number;
startHour?: number;
};
export type CreateRepeatsParams = {
companyId: string;
employeeId: string;
serviceId: string;
clientId: string;
discountId?: string;
weekDay: number;
startHour: number;
sessionUser: string;
};
export type DeleteRepeatsParams = {
repeatId: string;
sessionUser: string;
};
export type CountRepeatsParams = {
companyId: string;
employeeId: string;
countDate: Date;
heatMapFraction: number;
};
export type AddRepeatDisabledDateParams = {
repeatId: string;
disableDate: Date;
sessionUser: string;
};
export interface IRepeat {
id?: string;
companyId: string;
employeeId: string;
serviceId: string;
clientId: string;
discountId?: string;
weekDay: number;
startHour: number;
length: number;
disabledDates?: Date[];
}
export interface RepeatView {
id: string;
weekDay: number;
weekDayName: string;
startHour: number;
startHourFormatted: string;
length: number;
companyId: string;
companyName: string;
employeeId: string;
employeeName: string;
employeeCalendarColor: string;
employeeAvatar: string;
serviceId: string;
serviceName: string;
discountId: string;
discountType: DiscountType;
discountValue: number;
discountName: string;
discountCode: string;
clientId: string;
clientName: string;
}
export type GetPendingRepeatsParams = {
companyId?: string;
employeeId?: string;
sessionUser: string;
filterBySessionUser?: boolean;
};
export interface PendingRepeatView extends RepeatView {
pendingDate: Date;
}
+90
View File
@@ -0,0 +1,90 @@
export type FindSchedulesDisabledParams = {
_id?: string;
companyId: string;
employeeId?: string;
startDate?: Date;
endDate?: Date;
sessionUser: string;
};
export type CheckSchedulesDisabledParams = {
companyId: string;
employeeId?: string;
dateDay: Date;
};
export type PaginateSchedulesDisabledParams = FindSchedulesDisabledParams & {
page: number;
limit: number;
};
export type PaginateSchedulesDisabledResults = {
data: ISchedulesDisabled[];
page: number;
pages: number;
};
export type CreateSchedulesDisabledParams = {
companyId: string;
employeeId: string;
startDate: Date;
endDate: Date;
sessionUser: string;
};
export type DeleteSchedulesDisabledParams = {
id: string;
companyId: string;
sessionUser: string;
};
export type EmployeeItem = {
employeeId: string;
};
export type DisableScheduleParams = {
companyId: string;
employees: EmployeeItem[];
startDate: Date;
endDate: Date;
sessionUser: string;
};
export type DeleteSchedulesDisabledByCompanyParams = {
companyId: string;
};
export type DeleteSchedulesDisabledByEmployeeParams = {
employeeId: string;
};
export interface ISchedulesDisabled {
id?: string;
_id: string;
companyId: string;
employeeId: string;
startDate: Date;
endDate: Date;
}
export interface SchedulesDisabledView {
id: string;
employeeId: string;
employeeFullName: string;
employeeEmail: string;
employeeAvatar: string;
employeeUserId: string;
startDate: Date;
endDate: Date;
enabled: boolean;
}
export interface SchedulesDisabledByCollaboratorView {
id: string;
employeeId: string;
employeeFullName: string;
employeeEmail: string;
employeeAvatar: string;
employeeUserId: string;
disabledItems: ISchedulesDisabled[];
}
@@ -0,0 +1,31 @@
export type FindSchedulesEnabledParams = {
_id?: string;
companyId: string;
employeeId?: string;
startDate: string;
endDate: string;
};
export type EmployeeItem = {
employeeId: string;
};
export type EnableScheduleParams = {
companyId: string;
employees: EmployeeItem[];
startDate: string;
endDate: string;
sessionUser: string;
};
export interface SchedulesEnabledView {
id: string;
employeeId: string;
employeeFullName: string;
employeeEmail: string;
employeeAvatar: string;
employeeUserId: string;
startDate: Date;
endDate: Date;
enabled: boolean;
}
+40
View File
@@ -0,0 +1,40 @@
import { ScheduleItem } from "./Collaborators.model";
export type FindSchedulesOverridesParams = {
_id?: string;
companyId?: string;
employeeId?: string;
date?: Date;
sessionUser?: string;
};
export type CreateSchedulesOverridesParams = {
companyId: string;
employeeId: string;
schedules: Array<ScheduleItem>;
date: Date;
sessionUser: string;
};
export type UpdateSchedulesOverridesParams = {
companyId: string;
employeeId: string;
schedules: Array<ScheduleItem>;
date: Date;
sessionUser: string;
};
export type DeleteSchedulesOverridesParams = {
id: string;
companyId: string;
sessionUser: string;
};
export interface ISchedulesOverrides {
_id?: string;
id?: string;
companyId: string;
employeeId: string;
schedules: Array<ScheduleItem>;
date: Date;
}
+24
View File
@@ -0,0 +1,24 @@
export interface IServerError {
readonly code: number;
readonly desc: string;
}
export class ApiValidationError implements IServerError {
readonly code: number;
readonly desc: string;
constructor(code: number, desc: string) {
this.code = code;
this.desc = desc;
}
}
export class ApiError extends ApiValidationError {
constructor(code: number, desc: string) {
super(code, desc);
}
format = (): string => {
return `Atención: ${this.desc}`;
};
}
+156
View File
@@ -0,0 +1,156 @@
import { PublicOrganizationView } from "./Company.model";
export type FindServicesParams = {
_id?: string;
name?: string;
description?: string;
companyId?: string;
categoryId?: number;
length?: number;
price?: number;
limit?: number;
};
export type CreateServiceParams = {
companyId: string;
categoryId?: number;
name: string;
description: string;
length: number;
originalPrice: number;
limit: number;
sessionUser: string;
};
export type UpdateServiceParams = {
id: string;
companyId: string;
categoryId?: number;
name?: string;
description?: string;
length?: number;
originalPrice?: number;
limit?: number;
image?: string;
color?: string;
fontColor?: string;
fontShadowColor?: string;
sessionUser: string;
};
export type PaginateServicesParams = {
query?: string;
categoryId?: number;
companyId?: string;
priceFrom?: number;
priceTo?: number;
lengthFrom?: number;
lengthTo?: number;
block?: string;
city?: string;
state?: string;
page: number;
limit: number;
published?: SERVICE_PUBLISHED_STATUS;
};
export type PaginatePublicServicesParams = FindServicesParams & {
page: number;
limit: number;
};
export type PublicServicesResults = {
data: PublicServiceView[];
page: number;
pages: number;
};
export enum SERVICE_PUBLISHED_STATUS {
PUBLISHED = "published",
PRIVATE = "private",
}
export type SetPublishedStatusParams = {
serviceId: string;
published: SERVICE_PUBLISHED_STATUS;
sessionUser: string;
};
export type ApplyDiscountParams = {
companyId: string;
serviceId: string;
discountId: string;
sessionUser: string;
};
export type DeleteDiscountParams = {
companyId: string;
serviceId: string;
sessionUser: string;
};
export interface PublicServiceView {
id: string;
name: string;
description: string;
length: number;
price: number;
originalPrice: number;
discountId?: string;
discountName?: string;
discountCode?: string;
discountType?: string;
discountValue?: number;
limit: number;
image: string;
organizationId: string;
organizationName: string;
categoryId: number;
color: string;
fontColor: string;
fontShadowColor: string;
banned?: boolean;
}
export interface PublicOrganizationServiceView {
service: PublicServiceView;
organization: PublicOrganizationView;
}
export interface IService {
id?: string;
companyId: string;
categoryId: number;
name: string;
description: string;
length: number;
price: number;
originalPrice: number;
limit: number;
image: string;
color: string;
fontColor: string;
fontShadowColor: string;
banned?: boolean;
}
export interface CompanyServiceView {
id: string;
name: string;
description: string;
length: number;
price: number;
originalPrice: number;
discountId?: string;
discountName?: string;
discountCode?: string;
discountType?: string;
discountValue?: number;
limit: number;
image: string;
color: string;
fontColor: string;
fontShadowColor: string;
published: SERVICE_PUBLISHED_STATUS;
banned?: boolean;
}
+15
View File
@@ -0,0 +1,15 @@
import { IMetrics } from "./Metrics.model";
import { ISubscriptionInfo } from "./Subscription.Model";
export interface ISession {
loged: boolean;
userId: string;
userName: string;
userFullName: string;
userEmail: string;
userToken: string;
avatar: string;
subscription: ISubscriptionInfo;
organizationSubscriptions: Record<string, ISubscriptionInfo | null>;
metrics: IMetrics;
}
+98
View File
@@ -0,0 +1,98 @@
import { IPlan } from "./Plans.model";
export enum MP_SUBS_STATUS {
PENDING = "pending",
AUTHORIZED = "authorized",
PAUSED = "paused",
CANCELLED = "cancelled",
}
const getSubStatusText = (status: string | undefined): string => {
switch (status) {
case MP_SUBS_STATUS.PENDING:
return "PENDIENTE";
case MP_SUBS_STATUS.AUTHORIZED:
return "ACTIVO";
case MP_SUBS_STATUS.PAUSED:
return "PAUSADA";
case MP_SUBS_STATUS.CANCELLED:
return "CANCELADA";
default:
return "ACTIVO";
}
};
export enum MP_PRE_APPROVAL_STATUS {
READY_TO_START = "ready_to_start",
NO_PAYER_EMAIL = "no_payer_email",
}
export { getSubStatusText };
export type CreateMPPreApprovalParams = {
planId: string;
sessionUser: string;
months: number;
};
export type CancellPlanSuscriptionParams = {
sessionUser: string;
};
export type MPPreApprovalResponse = {
init_point: string;
status: MP_PRE_APPROVAL_STATUS;
};
export type GetSubscriptionInitPointParams = {
sessionUser: string;
};
export interface GetSuscriptionInitPointResponse {
init_point: string;
}
export interface ISubscriptionInfo {
id?: string;
plan: IPlan;
startDate?: Date;
endDate?: Date;
isActive?: boolean;
autoRenew?: boolean;
mpStatus?: string;
mpDateCreated?: Date;
}
export const DefaultSubscriptionModel: ISubscriptionInfo = {
id: "",
plan: {
name: "",
description: "",
features: [],
code: "",
price: 0,
annualPrice: 0,
id: "",
mailNotifications: false,
smsNotifications: false,
wapNotifications: false,
bot: false,
active: false,
dateLimit: false,
limitOrganizations: 0,
limitEmployees: 0,
limitServices: 0,
limitAppointments: 0,
limitClients: 0,
limitRepeats: 0,
payments: false,
discount3Months: 0,
discount6Months: 0,
discount12Months: 0,
},
isActive: false,
mpDateCreated: new Date(),
mpStatus: "",
startDate: new Date(),
endDate: new Date(),
autoRenew: false,
};
@@ -0,0 +1,84 @@
export type FindSystemNotificationsParams = {
_id?: string;
userId?: string;
companyId?: string;
serviceId?: string;
conversationId?: string;
notificationDate?: Date;
readed?: boolean;
sessionUser: string;
};
export type ToggleStateSystemNotificationsParams = {
state: boolean;
sessionUser: string;
};
export type PaginateSystemNotificationsParams = {
query?: string;
userId?: string;
companyId?: string;
serviceId?: string;
conversationId?: string;
dateFrom?: Date;
dateTo?: Date;
readed?: boolean;
page: number;
limit: number;
sessionUser: string;
};
export type PaginateSystemNotificationsResults = {
data: ISystemNotification[];
page: number;
pages: number;
};
export type UnreadNotificationResults = {
count: number;
};
export type UpdateSystemNotificationParams = {
id: string;
readed: boolean;
sessionUser: string;
};
export type DeleteSystemNotificationParams = {
id: string;
sessionUser: string;
};
export type DeleteNotificationsByDateParams = {
userId: string;
dateFrom?: Date;
dateTo?: Date;
sessionUser: string;
};
export enum NotificationType {
APPOINTMENT = 'APPOINTMENT',
COMPANY = 'COMPANY',
EMPLOYEE = 'EMPLOYEE',
SCHEDULE = 'SCHEDULE',
SCHEDULE_EXCEPTION = 'SCHEDULE_EXCEPTION',
SCHEDULE_RESTRICTION = 'SCHEDULE_RESTRICTION',
MESSAGE = 'MESSAGE',
BILLING = 'BILLING',
SYSTEM = 'SYSTEM'
}
export interface ISystemNotification {
id: string;
_id: string;
userId?: string;
companyId?: string;
serviceId?: string;
conversationId?: string;
subject: string;
message: string;
notificationDate: Date;
readed: boolean;
type?: NotificationType | string;
code?: string;
}
+91
View File
@@ -0,0 +1,91 @@
export enum TemplateTypes {
WAP_NEW_APPOINTMENT = "wap-new-appointment",
WAP_REMINDER = "wap-reminder",
EMAIL_NEW_APPOINTMENT = "email-new-appointment",
EMAIL_REMINDER = "email-reminder",
}
export const refactorFields = (text: string) => {
let newText = text;
newText = newText.replaceAll("@tx.Cliente", "<---CLIENTE--->");
newText = newText.replaceAll("@tx.Fecha", "<---FECHA--->");
newText = newText.replaceAll("@tx.Horario", "<---HORARIO--->");
newText = newText.replaceAll("@tx.Direccion", "<---DIRECCION--->");
newText = newText.replaceAll("@tx.Professional", "<---PROFESIONAL--->");
newText = newText.replaceAll("@tx.Servicio", "<---SERVICIO--->");
newText = newText.replaceAll("@tx.Precio", "<---PRECIO--->");
newText = newText.replaceAll("@tx.Organizacion", "<---ORGANIZACION--->");
newText = newText.replaceAll("@tx.Salto", "<---BR--->");
return newText;
};
export const unRefactorFields = (text: string) => {
let newText = text;
newText = newText.replaceAll("<---CLIENTE--->", "@tx.Cliente");
newText = newText.replaceAll("<---FECHA--->", "@tx.Fecha");
newText = newText.replaceAll("<---HORARIO--->", "@tx.Horario");
newText = newText.replaceAll("<---DIRECCION--->", "@tx.Direccion");
newText = newText.replaceAll("<---PROFESIONAL--->", "@tx.Professional");
newText = newText.replaceAll("<---SERVICIO--->", "@tx.Servicio");
newText = newText.replaceAll("<---PRECIO--->", "@tx.Precio");
newText = newText.replaceAll("<---ORGANIZACION--->", "@tx.Organizacion");
newText = newText.replaceAll("<---BR--->", "@tx.Salto");
return newText;
};
export interface ITemplate {
id: string;
_id: string;
companyId: string;
name: string;
code: string;
type: TemplateTypes;
template: string;
createdAt: Date;
}
export type FindTemplatesParams = {
id?: string;
companyId: string;
code?: string;
name?: string;
type?: TemplateTypes;
sessionUser: string;
};
export type PaginateTemplatesParams = FindTemplatesParams & {
page: number;
limit: number;
};
export type PaginateTemplatesResults = {
data: ITemplate[];
page: number;
pages: number;
};
export type SaveTemplateParams = {
templateId?: string;
companyId: string;
name: string;
code: string;
type: TemplateTypes;
template: string;
sessionUser: string;
};
export type DeleteTemplateParams = {
id: string;
companyId: string;
sessionUser: string;
};
export type GetDefaultTemplateParams = {
type: TemplateTypes;
companyId: string;
sessionUser: string;
};
@@ -0,0 +1,11 @@
export type TextObjectFilterResult = {
id: string;
name: string;
description?: string;
image?: string;
data?: string;
};
export type TOFClientsParams = {
companyId: string;
};
+109
View File
@@ -0,0 +1,109 @@
export type SignUpParams = {
email: string;
password: string;
lastName: string;
firstName: string;
phoneCountryCode?: string;
phoneAreaCode: string;
phoneNumber: string;
};
export interface IUserView {
id: string;
email: string;
firstName: string;
lastName: string;
fullName: string;
avatar: string;
phoneNumber: string;
phoneCountryCode: string;
phoneAreaCode: string;
formattedPhoneNumber: string;
street: string;
streetNumber: string;
builingFloor: string;
buildingApartament: string;
block: string;
city: string;
state: string;
country: string;
zipCode: string;
}
export interface CollaboratorView {
id: string;
email: string;
fullName: string;
avatar: string;
}
export interface UserAssistanceInfo {
message: string;
email?: string;
fullName?: string;
verificated: boolean;
activationCode?: string;
recoveryCode?: string;
}
export type FindAssistanceUserParams = {
email: string;
sessionUser: string;
};
export type FindCollaboratorByEmailParams = {
email: string;
};
export type UpdateUserParams = {
id: string;
firstName?: string;
lastName?: string;
street?: string;
streetNumber?: string;
builingFloor?: string;
buildingApartament?: string;
block?: string;
city?: string;
state?: string;
country?: string;
zipCode?: string;
phoneCountryCode?: string;
phoneAreaCode?: string;
phoneNumber?: string;
sessionUser: string;
};
export type ChangePasswordParams = {
oldPassword: string;
newPassword: string;
sessionUser: string;
};
export type RecoveryAccountParams = {
email: string;
recoveryCode?: string;
newPassword?: string;
sendRecoveryCode: boolean;
};
export type DeleteUserParams = {
sessionUser: string;
};
export type SavePayerEmailParams = {
email: string;
sessionUser: string;
};
export type GetPayerEmailParams = {
sessionUser: string;
};
export type CheckUserPhoneParams = {
sessionUser: string;
};
export interface PayerEmailResult {
mpPayerEmail?: string;
}
+21
View File
@@ -0,0 +1,21 @@
import { AppointmentEventByClient } from "./Appointments.model";
import { PublicOrganizationView } from "./Company.model";
import { PublicServicesResults } from "./Service.model";
export type HomeView = {
education: PublicServicesResults;
health: PublicServicesResults;
economy: PublicServicesResults;
construction: PublicServicesResults;
other: PublicServicesResults;
};
export type UserAppointmentViewParams = {
appointmentId: string;
sessionUser: string;
};
export type UserAppointmentView = {
appointment: AppointmentEventByClient;
organization: PublicOrganizationView;
};
+75
View File
@@ -0,0 +1,75 @@
export type AssingAvailableServerParams = {
companyId: string;
sessionUser: string;
};
export type BotEventParams = AssingAvailableServerParams & {};
export type BotRunningStatus =
| "created"
| "running"
| "paused"
| "restarting"
| "removing"
| "exited"
| "dead";
export type BotEventError = {
error: string;
code: number;
};
export enum SERVER_STATE {
"RUNNING" = "running",
"NOT_WORKING" = "not_working",
"NOT_ASSIGNED" = "not_assigned",
}
export enum BOT_STATE {
"EXISTS" = "exists",
"NOT_EXISTS" = "not_exists",
}
export enum BOT_SESSION_STATUS {
"LOGED" = "ok",
"UNLOGED" = "fail",
}
export type BotCheckStatus = {
state: BOT_STATE;
status: BotRunningStatus;
serverState: SERVER_STATE;
};
export type BotCheckSession = {
session: BOT_SESSION_STATUS;
};
export type SendBotMessageParams = {
companyId: string;
number: string;
message: string;
sessionUser: string;
};
export interface BotView {
exists: boolean;
runningState: BotRunningStatus;
serverId?: string;
serverName?: string;
serverDescription?: string;
serverState?: SERVER_STATE;
botSession?: BOT_SESSION_STATUS;
automaticNotifications: boolean;
}
export interface IWapServer {
id?: string;
name?: string;
description?: string;
ipv4: string;
ipv6: string;
countBots: number;
maxBots: number;
active: boolean;
}
@@ -0,0 +1,32 @@
import { ApiError } from "@core/Models/Server.Error.model";
class ApiServerService {
post<T>(endpoint: string, data: object): Promise<T> {
return new Promise<T>((resolve, reject) => {
const url = process.env.NEXT_PUBLIC_API_URL;
fetch(`${url}${endpoint}`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
cache: "no-store",
})
.then((response) => {
resolve(response.json());
})
.catch((error) => {
try {
reject(new ApiError(error.response.data.code, error.response.data.desc));
} catch {
const errorMessage = process.env.VITE_API_UNAVAILABLE as string;
reject(new ApiError(500, errorMessage));
}
});
});
}
}
const API_SERVER = new ApiServerService();
export default API_SERVER;
+64
View File
@@ -0,0 +1,64 @@
import { getTokenFromLocalStorage } from "@config/token";
import { ApiError } from "@models/Server.Error.model";
import axios from "@config/axios.config";
class ApiService {
/**
* Sends a HTTP POST request to the specified endpoint with the provided data
* and returns a promise that resolves to the response data.
*
* @param {string} endpoint - The URL or path of the API endpoint to send the request to.
* @param {object} data - The data to be sent with the request.
* @return {Promise<T>} A promise that resolves to the response data of type T.
*/
post<T>(endpoint: string, data: object): Promise<T> {
return new Promise<T>((resolve, reject) => {
axios()
.post<T>(endpoint, data)
.then((response) => {
resolve(response.data);
})
.catch((error) => {
try {
const data = error.response.data;
const msg = data.desc || data.message || JSON.stringify(data.details || data);
reject(new ApiError(data.code || error.response.status || 500, msg));
} catch {
const errorMessage = process.env.VITE_API_UNAVAILABLE as string;
reject(new ApiError(500, errorMessage));
}
});
});
}
postFile<T>(endpoint: string, data: FormData): Promise<T> {
return new Promise<T>((resolve, reject) => {
axios()
.post<T>(endpoint, data, {
headers: {
"content-type": "multipart/form-data",
Authorization: getTokenFromLocalStorage(),
},
})
.then((response) => {
resolve(response.data);
})
.catch((error) => {
try {
console.error("Full API Error:", error.response?.data);
const data = error.response.data;
const msg = data.desc || data.message || JSON.stringify(data.details || data);
reject(new ApiError(data.code || error.response.status || 500, msg));
} catch (e) {
console.error(e);
const errorMessage = process.env.VITE_API_UNAVAILABLE as string;
reject(new ApiError(500, errorMessage));
}
});
});
}
}
const API = new ApiService();
export default API;
@@ -0,0 +1,15 @@
import { ApiVoidResult } from "@core/Models/Api.VoidResult.type";
import ApiRequest from "@services/Api.Service";
export type ErrorReportParams = {
message: string;
url: string;
userName: string;
userEmail: string;
userId: string;
dateError: Date;
};
export const reportError = async (data: ErrorReportParams): Promise<ApiVoidResult> => {
return ApiRequest.post<ApiVoidResult>("error-reporting/report", data);
};
+89
View File
@@ -0,0 +1,89 @@
import { ISession } from "@models/Session.model";
import ApiRequest from "./Api.Service";
import { ApiVoidResult } from "@core/Models/Api.VoidResult.type";
import { CheckUserPhoneParams, RecoveryAccountParams } from "@core/Models/User.model";
/**
* Logs in a user with the given email and password.
* @param {string} email - The user's email.
* @param {string} password - The user's password.
* @returns {Promise<ISession>} - The session object containing the user's session details.
*/
export async function login(email: string, password: string): Promise<ISession> {
// Send a POST request to the "users/login" endpoint with the email and password
return ApiRequest.post<ISession>("users/login", {
email: email,
password: password,
});
}
/**
* Logs in a user using a token.
*
* @param {string} token - The token to authenticate the user.
* @returns {Promise<ISession>} - A promise that resolves to a session object if the login is successful, or null if it fails.
*/
export async function loginByToken(token: string): Promise<ISession> {
// Make a POST request to the "users/loginbytoken" endpoint with the provided token
return ApiRequest.post<ISession>("users/loginbytoken", {
token: token,
});
}
export async function checkUserPhone(data: CheckUserPhoneParams): Promise<boolean> {
// Make a POST request to the "users/loginbytoken" endpoint with the provided token
return ApiRequest.post<boolean>("users/check-phone", data);
}
export async function loginByGoogle(token: string): Promise<ISession> {
// Make a POST request to the "users/loginbytoken" endpoint with the provided token
return ApiRequest.post<ISession>("users/loginbygoogle", {
token: token,
});
}
export async function loginByAndroid(token: string): Promise<ISession> {
// Make a POST request to the "users/loginbytoken" endpoint with the provided token
return ApiRequest.post<ISession>("users/loginbyandroid", {
token: token,
});
}
export async function loginAndVerificate(
email: string,
password: string,
verificationCode: string
): Promise<ISession> {
// Make a POST request to the "users/loginbytoken" endpoint with the provided token
return ApiRequest.post<ISession>("users/verificate", {
email,
password,
verificationCode,
});
}
export async function sendRecoveryCode(email: string): Promise<ApiVoidResult> {
// Make a POST request to the "users/loginbytoken" endpoint with the provided token
const data: RecoveryAccountParams = {
email,
sendRecoveryCode: true,
};
return ApiRequest.post<ApiVoidResult>("users/recovery", data);
}
export async function recoveryAccountWithCode(
email: string,
newPassword: string,
recoveryCode: string
): Promise<ApiVoidResult> {
// Make a POST request to the "users/loginbytoken" endpoint with the provided token
const data: RecoveryAccountParams = {
email,
newPassword,
recoveryCode: recoveryCode,
sendRecoveryCode: false,
};
return ApiRequest.post<ApiVoidResult>("users/recovery", data);
}
+15
View File
@@ -0,0 +1,15 @@
// socket.ts
import { io, Socket } from "socket.io-client";
export let clientSocket: Socket | undefined;
export const getClientSocket = (userId: string) => {
const serverUrl = process.env.NEXT_PUBLIC_CHAT_URL;
clientSocket = io(serverUrl, {
auth: {
userId,
},
});
return clientSocket;
};
+68
View File
@@ -0,0 +1,68 @@
import { create } from "zustand";
export enum ALERT_MODES {
SUCCESS = "success",
ERROR = "error",
WARNING = "warning",
INFO = "info",
}
interface IAlertStore {
visible: boolean;
message: string;
mode: ALERT_MODES;
hiddingTimeout: number;
setVisible: (state: boolean) => void;
showSuccess: (message: string) => void;
showError: (message: string) => void;
showWarning: (message: string) => void;
showInfo: (message: string) => void;
clear: () => void;
}
export type AlertState = IAlertStore;
const useAlert = create<AlertState>()((set) => ({
visible: false,
message: "",
mode: ALERT_MODES.SUCCESS,
hiddingTimeout: 6000,
setVisible: (state: boolean) => set(() => ({ visible: state })),
showSuccess: (message: string) =>
set(() => ({
message: message,
visible: true,
mode: ALERT_MODES.SUCCESS,
hiddingTimeout: 1000,
})),
showError: (message: string) =>
set(() => ({
message: message,
visible: true,
mode: ALERT_MODES.ERROR,
hiddingTimeout: 6000,
})),
showWarning: (message: string) =>
set(() => ({
message: message,
visible: true,
mode: ALERT_MODES.WARNING,
hiddingTimeout: 6000,
})),
showInfo: (message: string) =>
set(() => ({
message: message,
visible: true,
mode: ALERT_MODES.INFO,
hiddingTimeout: 6000,
})),
clear: () =>
set(() => ({
visible: false,
message: "",
mode: ALERT_MODES.SUCCESS,
hiddingTimeout: 6000,
})),
}));
export { useAlert };
@@ -0,0 +1,20 @@
import { create } from "zustand";
interface IAppointmentsViewStore {
collaboratorId: string;
setCollaboratorId: (id: string) => void;
clear: () => void;
}
export type AppointmentsState = IAppointmentsViewStore;
const useAppointmentsViewStore = create<AppointmentsState>()((set) => ({
collaboratorId: "",
setCollaboratorId: (id: string) => set(() => ({ collaboratorId: id })),
clear: () =>
set(() => ({
collaboratorId: "",
})),
}));
export { useAppointmentsViewStore };
+21
View File
@@ -0,0 +1,21 @@
import React from "react";
import { create } from "zustand";
interface IBottomToolbarStore {
toolbar: React.ReactNode;
set: (item: React.ReactNode) => void;
clear: () => void;
}
export type BottomToolbarState = IBottomToolbarStore;
const useBottomToolbarStore = create<BottomToolbarState>()((set) => ({
toolbar: undefined,
set: (item: React.ReactNode) => set(() => ({ toolbar: item })),
clear: () =>
set(() => ({
toolbar: undefined,
})),
}));
export { useBottomToolbarStore };
+26
View File
@@ -0,0 +1,26 @@
import { persist, createJSONStorage } from "zustand/middleware";
import { create } from "zustand";
export type ButtonPanelMode = "icon" | "button";
export interface ILoginActionStore {
mode: ButtonPanelMode;
setMode: (mod: ButtonPanelMode) => void;
clear: () => void;
}
const useButtonPanelStore = create<ILoginActionStore>()(
persist(
(set) => ({
mode: "button",
setMode: (mod: ButtonPanelMode) => set(() => ({ mode: mod })),
clear: () => set(() => ({ mode: "button" })),
}),
{
name: "site-panel-mode",
storage: createJSONStorage(() => localStorage),
}
)
);
export { useButtonPanelStore };
+24
View File
@@ -0,0 +1,24 @@
import { create } from "zustand";
import { TXCalendarEvent } from "@core/app/theme/scheduleView";
interface IClipboardStore {
copiedAppointment: TXCalendarEvent | null;
setCopiedAppointment: (event: TXCalendarEvent | null) => void;
clear: () => void;
}
export type ClipboardState = IClipboardStore;
const useClipboardStore = create<ClipboardState>()((set) => ({
copiedAppointment: null,
setCopiedAppointment: (event: TXCalendarEvent | null) =>
set(() => ({
copiedAppointment: event,
})),
clear: () =>
set(() => ({
copiedAppointment: null,
})),
}));
export { useClipboardStore };
+48
View File
@@ -0,0 +1,48 @@
import { create } from "zustand";
interface IConfirmStore {
visible: boolean;
message: string;
title: string;
onConfirm: () => void;
onCancel: () => void;
show: (message: string, onConfirm: () => void, onCancel: () => void) => void;
close: () => void;
clear: () => void;
}
export type ConfirmState = IConfirmStore;
const useConfirmStore = create<ConfirmState>()((set) => ({
visible: false,
message: "",
title: "",
onConfirm: () => {},
onCancel: () => {},
show: (message: string, onConfirm: () => void, onCancel: () => void) =>
set(() => ({
visible: true,
message: message,
onConfirm: onConfirm,
onCancel: onCancel,
title: "Atención",
})),
close: () =>
set(() => ({
visible: false,
message: "",
onConfirm: () => {},
onCancel: () => {},
title: "",
})),
clear: () =>
set(() => ({
visible: false,
message: "",
onConfirm: () => {},
onCancel: () => {},
title: "",
})),
}));
export { useConfirmStore };
+31
View File
@@ -0,0 +1,31 @@
import { create } from "zustand";
export enum EVENT_TYPES {
SAVE = "save",
ADD = "add",
CLEAR = "clear",
FILTER = "filter",
SLEEP = "sleep",
NEED_SAVE = "need-save",
NEED_ADD = "need-add",
LOADING = "loading",
}
interface IEventHandlerStore {
eventType: EVENT_TYPES;
setEventType: (state: EVENT_TYPES) => void;
clear: () => void;
}
export type EventHandlerState = IEventHandlerStore;
const useEventHandlerStore = create<EventHandlerState>()((set) => ({
eventType: EVENT_TYPES.SLEEP,
setEventType: (state: EVENT_TYPES) => set(() => ({ eventType: state })),
clear: () =>
set(() => ({
eventType: EVENT_TYPES.SLEEP,
})),
}));
export { useEventHandlerStore };
+49
View File
@@ -0,0 +1,49 @@
import React from "react";
import { create } from "zustand";
export interface FilterData {
id?: string;
}
interface IFilterStore {
filter: React.ReactNode;
visible: boolean;
title: string;
filterData: FilterData;
onFilter: () => void;
setContent: (item: React.ReactNode) => void;
setOnFilter: (onFilter: () => void) => void;
setVisible: (state: boolean) => void;
setTitle: (title: string) => void;
setFilterData: (data: FilterData) => void;
toggle: () => void;
clear: () => void;
clearFilterData: () => void;
}
export type FilterState = IFilterStore;
const useFilterStore = create<FilterState>()((set) => ({
filter: undefined,
visible: false,
title: "",
filterData: {},
onFilter: () => {},
setOnFilter: (onFilter: () => void) => set(() => ({ onFilter })),
setContent: (item: React.ReactNode) => set(() => ({ filter: item })),
setVisible: (state: boolean) => set(() => ({ visible: state })),
setTitle: (title: string) => set(() => ({ title: title })),
setFilterData: (data: FilterData) => set(() => ({ filterData: data })),
toggle: () => set((state) => ({ visible: !state.visible })),
clear: () =>
set(() => ({
filter: undefined,
visible: false,
title: "",
onFilter: () => {},
filterData: {},
})),
clearFilterData: () => set(() => ({ filterData: {} })),
}));
export { useFilterStore };
+62
View File
@@ -0,0 +1,62 @@
import { NO_CATEGORY } from "@core/Models/Categories.type";
import { create } from "zustand";
export enum HEADER_MODES {
LARGE = "large",
SMALL = "small",
}
export enum HEADER_WIDGETS {
FIND = "find",
PUBLIC_FIND = "public-find",
NONE = "none",
}
interface IHeaderStore {
notificationsCount: number;
state: HEADER_MODES;
activeWidget: HEADER_WIDGETS;
findText: string;
publicFindText: string;
publicFindCategoryId: number;
publicFindTrigger: boolean;
setNotificationsCount: (count: number) => void;
setState: (state: HEADER_MODES) => void;
setFindText: (text: string) => void;
setPublicFindText: (text: string) => void;
setPublicFindCategoryId: (id: number) => void;
setActiveWidget: (widget: HEADER_WIDGETS) => void;
setPublicFindTrigger: (trigger: boolean) => void;
clear: () => void;
}
export type HeaderState = IHeaderStore;
const useHeaderStore = create<HeaderState>()((set) => ({
notificationsCount: 0,
state: HEADER_MODES.LARGE,
activeWidget: HEADER_WIDGETS.PUBLIC_FIND,
findText: "",
publicFindText: "",
publicFindCategoryId: NO_CATEGORY,
publicFindTrigger: false,
setNotificationsCount: (count: number) => set(() => ({ notificationsCount: count })),
setState: (state: HEADER_MODES) => set(() => ({ state: state })),
setFindText: (text: string) => set(() => ({ findText: text })),
setPublicFindText: (text: string) => set(() => ({ publicFindText: text })),
setPublicFindCategoryId: (id: number) => set(() => ({ publicFindCategoryId: id })),
setActiveWidget: (widget: HEADER_WIDGETS) => set(() => ({ activeWidget: widget })),
setPublicFindTrigger: (trigger: boolean) => set(() => ({ publicFindTrigger: trigger })),
clear: () =>
set(() => ({
notificationsCount: 0,
state: HEADER_MODES.LARGE,
activeWidget: HEADER_WIDGETS.PUBLIC_FIND,
findText: "",
publicFindText: "",
publicFindCategoryId: NO_CATEGORY,
publicFindTrigger: false,
})),
}));
export { useHeaderStore };
+31
View File
@@ -0,0 +1,31 @@
import { create } from "zustand";
export type LocationItem = {
url: string;
title: string;
icon?: React.ReactNode;
};
interface ILocationStore {
path: LocationItem[];
add: (item: LocationItem) => void;
last: () => LocationItem | undefined;
clear: () => void;
}
export type LocationState = ILocationStore;
const useLocationStore = create<LocationState>()((set, get) => ({
path: [],
add: (item: LocationItem) => set((state) => ({ path: [...state.path, item] })),
last: () => {
const path = get().path;
return path.length - 1 >= 0 ? path[path.length - 1] : undefined; // Retorna el último elemento o `undefined` si el array está vacío
},
clear: () =>
set(() => ({
path: [],
})),
}));
export { useLocationStore };
+35
View File
@@ -0,0 +1,35 @@
import { persist, createJSONStorage } from "zustand/middleware";
import { create } from "zustand";
export enum LOGIN_ACTIONS {
PLAN_SUBSCRIPTION = "plan-subscription",
ORGANIZATION_CREATE = "organization-create",
APPOINTMENT_CREATE = "appointment-create",
NONE = "none",
}
export interface ILoginActionStore {
action: LOGIN_ACTIONS;
url: string;
setUrl: (url: string) => void;
setLoginAction: (action: LOGIN_ACTIONS) => void;
clear: () => void;
}
const useLoginActionStore = create<ILoginActionStore>()(
persist(
(set) => ({
action: LOGIN_ACTIONS.NONE,
url: "/",
setLoginAction: (action: LOGIN_ACTIONS) => set(() => ({ action: action })),
setUrl: (url: string) => set(() => ({ url: url })),
clear: () => set(() => ({ action: LOGIN_ACTIONS.NONE, url: "/" })),
}),
{
name: "site-login-action",
storage: createJSONStorage(() => localStorage),
}
)
);
export { useLoginActionStore };
+49
View File
@@ -0,0 +1,49 @@
import { create } from "zustand";
export type MenuItem = {
text?: string;
icon?: React.ReactNode;
onClick?: () => void;
divider?: boolean;
};
interface IMenuStore {
items: MenuItem[];
visible: boolean;
anchorEl: HTMLElement | null;
add: (item: MenuItem) => void;
setAnchorEl: (el: HTMLElement) => void;
divider: () => void;
show: () => void;
hide: () => void;
clear: () => void;
length: () => number;
}
export type MenuState = IMenuStore;
const useMenuStore = create<MenuState>()((set, get) => ({
items: [],
visible: false,
anchorEl: null,
add: (item: MenuItem) => set((state) => ({ items: [...state.items, item] })),
setAnchorEl: (el: HTMLElement) => set(() => ({ anchorEl: el })),
divider: () => set((state) => ({ items: [...state.items, { divider: true }] })),
show: () =>
set(() => ({
visible: true,
})),
hide: () =>
set(() => ({
visible: false,
})),
clear: () =>
set(() => ({
items: [],
visible: false,
})),
length: () => get().items.length,
}));
export { useMenuStore };
@@ -0,0 +1,20 @@
import { PublicOrganizationView } from "@core/Models/Company.model";
import { create } from "zustand";
export type LoginState = "started" | "success" | "sleep" | "error";
interface IOrgDataStore {
orgData: PublicOrganizationView | null;
setData: (data: PublicOrganizationView) => void;
clear: () => void;
}
const useOrgDataStore = create<IOrgDataStore>()((set) => ({
orgData: null,
setData: (data: PublicOrganizationView) => set(() => ({ orgData: data })),
clear: () =>
set(() => ({
orgData: null,
})),
}));
export { useOrgDataStore };
@@ -0,0 +1,56 @@
import { persist, createJSONStorage } from "zustand/middleware";
import { create } from "zustand";
export enum ORGANIZATION_HEADER_MODES {
LARGE = "large",
SMALL = "small",
}
interface IOrganizationHeaderStore {
state: ORGANIZATION_HEADER_MODES;
headerColor: string;
headerFontColor: string;
headerFontShadowColor: string;
setState: (state: ORGANIZATION_HEADER_MODES) => void;
setHeaderColor: (color: string) => void;
setHeaderFontColor: (color: string) => void;
setHeaderFontShadowColor: (color: string) => void;
toggleState: () => void;
clear: () => void;
}
export type OrganizationHeaderState = IOrganizationHeaderStore;
const useOrganizationHeaderStore = create<OrganizationHeaderState>()(
persist(
(set) => ({
state: ORGANIZATION_HEADER_MODES.LARGE,
headerColor: "",
headerFontColor: "",
headerFontShadowColor: "",
setState: (state: ORGANIZATION_HEADER_MODES) => set(() => ({ state: state })),
setHeaderColor: (color: string) => set(() => ({ headerColor: color })),
setHeaderFontColor: (color: string) => set(() => ({ headerFontColor: color })),
setHeaderFontShadowColor: (color: string) =>
set(() => ({ headerFontShadowColor: color })),
toggleState: () =>
set((state) => ({
state:
state.state === ORGANIZATION_HEADER_MODES.LARGE
? ORGANIZATION_HEADER_MODES.SMALL
: ORGANIZATION_HEADER_MODES.LARGE,
})),
clear: () =>
set(() => ({
state: ORGANIZATION_HEADER_MODES.LARGE,
})),
}),
{
name: "site-organization-header-state",
storage: createJSONStorage(() => localStorage),
}
)
);
export { useOrganizationHeaderStore };
+116
View File
@@ -0,0 +1,116 @@
import { ISession } from "@models/Session.model";
import { persist, createJSONStorage } from "zustand/middleware";
import ApiRequest from "@services/Api.Service";
import { create } from "zustand";
import { DefaultMetricsModel, IMetrics } from "@core/Models/Metrics.model";
import { DefaultSubscriptionModel, ISubscriptionInfo } from "@core/Models/Subscription.Model";
export type LoginState = "started" | "success" | "sleep" | "error";
interface ISessionStore {
loginState: LoginState;
loading: boolean;
setLoginState: (state: LoginState) => void;
setLoged: (state: boolean) => void;
setUserId: (userId: string) => void;
setUserFullName: (userFullName: string) => void;
setUserEmail: (userEmail: string) => void;
setUser: (sessionData: ISession) => void;
setSubscription: (subscription: ISubscriptionInfo) => void;
setMetrics: (metrics: IMetrics) => void;
checkSession: () => Promise<void>;
clear: () => void;
clearSubscription: () => void;
}
export type SessionState = ISession & ISessionStore;
const useSessionStore = create<SessionState>()((set) => ({
loged: false,
userId: "",
userName: "",
userFullName: "",
userEmail: "",
avatar: "",
subscription: DefaultSubscriptionModel,
organizationSubscriptions: {},
metrics: DefaultMetricsModel,
userToken: "",
loginState: "sleep",
loading: true,
setLoginState: (state: LoginState) => set(() => ({ loginState: state })),
setLoged: (state: boolean) => set(() => ({ loged: state })),
setUserId: (userId: string) => set(() => ({ userId: userId })),
setUserFullName: (userFullName: string) => set(() => ({ userFullName: userFullName })),
setUserEmail: (userEmail: string) => set(() => ({ userEmail: userEmail })),
setUser: (user: ISession) => set(() => ({ ...user })),
setSubscription: (subscription: ISubscriptionInfo) =>
set(() => ({ subscription: subscription })),
setMetrics: (metrics: IMetrics) => set(() => ({ metrics: metrics })),
checkSession: async () => {
const token = useSessionTokenStore.getState().token;
if (!token) {
set({ loading: false });
return;
}
try {
const sessionData = await ApiRequest.post<ISession>("users/loginbytoken", {
token,
});
set({ ...sessionData, loginState: "success" });
useSessionTokenStore.getState().setToken(sessionData.userToken);
} catch {
set({
loged: false,
loginState: "sleep",
userId: "",
userEmail: "",
userName: "",
userFullName: "",
avatar: "",
subscription: DefaultSubscriptionModel,
organizationSubscriptions: {},
metrics: DefaultMetricsModel,
});
useSessionTokenStore.getState().setToken("");
} finally {
set({ loading: false });
}
},
clear: () =>
set(() => ({
loged: false,
loginState: "sleep",
userId: "",
userEmail: "",
userName: "",
userFullName: "",
avatar: "",
subscription: DefaultSubscriptionModel,
organizationSubscriptions: {},
metrics: DefaultMetricsModel,
})),
clearSubscription: () => set(() => ({ subscription: DefaultSubscriptionModel })),
}));
export interface ISessionTokenStore {
token: string;
setToken: (token: string) => void;
}
const useSessionTokenStore = create<ISessionTokenStore>()(
persist(
(set) => ({
token: "",
setToken: (token: string) => set(() => ({ token: token })),
}),
{
name: "site-session-token",
storage: createJSONStorage(() => localStorage),
}
)
);
export { useSessionStore, useSessionTokenStore };
@@ -0,0 +1,51 @@
import { TextObjectFilterResult } from "@models/TextObjectFilter.model";
import { create } from "zustand";
export type TextObjectModalContent = {
header?: React.ReactNode;
bottom?: React.ReactNode;
renderResult: (result: TextObjectFilterResult, index: number) => React.ReactNode;
};
interface ITextObjectModalStore {
content: TextObjectModalContent;
data: TextObjectFilterResult[];
endpoint: string;
onSelected: (item: TextObjectFilterResult) => void;
setContent: (content: TextObjectModalContent) => void;
setData: (data: TextObjectFilterResult[]) => void;
setEndPoint: (endpoint: string) => void;
setOnSelected: (onSelected: (item: TextObjectFilterResult) => void) => void;
clear: () => void;
}
export type TextObjectModalState = ITextObjectModalStore;
const useTextObjectModalStore = create<TextObjectModalState>()((set) => ({
content: {
header: undefined,
bottom: undefined,
renderResult: () => undefined,
},
data: [],
endpoint: "",
onSelected: () => {},
setContent: (content: TextObjectModalContent) => set(() => ({ content: content })),
setData: (data: TextObjectFilterResult[]) => set(() => ({ data: data })),
setEndPoint: (endpoint: string) => set(() => ({ endpoint: endpoint })),
setOnSelected(onSelected: (item: TextObjectFilterResult) => void) {
set(() => ({ onSelected: onSelected }));
},
clear: () =>
set(() => ({
content: {
header: undefined,
bottom: undefined,
renderResult: () => undefined,
},
data: [],
endpoint: "",
})),
}));
export { useTextObjectModalStore };
+55
View File
@@ -0,0 +1,55 @@
import dayjs from "dayjs";
import "dayjs/locale/es";
import { create } from "zustand";
dayjs.locale("es");
interface ITimePickerStore {
visible: boolean;
timeFraction: number;
value: dayjs.Dayjs;
defaultValue: dayjs.Dayjs;
onAccept: (newDay: dayjs.Dayjs) => void;
setTimeFraction: (timeFraction: number) => void;
setValue: (value: dayjs.Dayjs) => void;
setDefaultValue: (value: dayjs.Dayjs) => void;
show: (onAccept: (newDay: dayjs.Dayjs) => void) => void;
hide: () => void;
}
export type TimePickerState = ITimePickerStore;
const useTimePickerStore = create<TimePickerState>()((set) => ({
visible: false,
timeFraction: 15,
value: dayjs(new Date()).hour(8).minute(0),
defaultValue: dayjs(new Date()),
onAccept: () => {},
setTimeFraction(timeFraction) {
set(() => ({
timeFraction: timeFraction,
}));
},
setDefaultValue(value) {
set(() => ({
defaultValue: value,
}));
},
setValue(value) {
set(() => ({
value: value,
}));
},
show: (onAccept: (newDay: dayjs.Dayjs) => void) =>
set(() => ({
visible: true,
onAccept: onAccept,
})),
hide: () =>
set(() => ({
visible: false,
onAccept: () => {},
})),
}));
export { useTimePickerStore };
+20
View File
@@ -0,0 +1,20 @@
import { create } from "zustand";
interface IUserMenuStore {
visible: boolean;
setVisible: (state: boolean) => void;
clear: () => void;
}
export type UserMenuState = IUserMenuStore;
const useUserMenuStore = create<UserMenuState>()((set) => ({
visible: false,
setVisible: (state: boolean) => set(() => ({ visible: state })),
clear: () =>
set(() => ({
visible: false,
})),
}));
export { useUserMenuStore };
+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>
</>
);
}

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