first commit
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export type ApiVoidResult = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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[];
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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}`;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
@@ -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 <></>;
|
||||
}
|
||||
+210
@@ -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);
|
||||
};
|
||||
+75
@@ -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;
|
||||
}
|
||||
+255
@@ -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> </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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+347
@@ -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> </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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+147
@@ -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"));
|
||||
});
|
||||
});
|
||||
};
|
||||
+828
@@ -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> </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> </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> </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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+60
@@ -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));
|
||||
});
|
||||
};
|
||||
+253
@@ -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> </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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+477
@@ -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> </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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+126
@@ -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> </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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+290
@@ -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> </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> </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> </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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+30
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+39
@@ -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> </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> </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> </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" }}> </div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+44
@@ -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" }}> </div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+54
@@ -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);
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import Saveable from "@components/Saveable/Saveable";
|
||||
|
||||
export default function CreateClientLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<Saveable>{children}</Saveable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+323
@@ -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> </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> </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> </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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
"use client";
|
||||
export default function AppointmentLayout({ children }: { children: React.ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
||||
+70
@@ -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;
|
||||
}
|
||||
+617
@@ -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> </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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+35
@@ -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);
|
||||
};
|
||||
+238
@@ -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> </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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+381
@@ -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> </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> </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> </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" }}> </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" }}> </div>
|
||||
</>
|
||||
</SecureByPlan>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+50
@@ -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);
|
||||
};
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import Saveable from "@components/Saveable/Saveable";
|
||||
|
||||
export default function CreateClientLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<>
|
||||
<Saveable>{children}</Saveable>
|
||||
</>
|
||||
);
|
||||
}
|
||||
+150
@@ -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
Reference in New Issue
Block a user