first commit

This commit is contained in:
2026-07-16 20:48:43 -03:00
commit 5696cee264
1111 changed files with 322270 additions and 0 deletions
+328
View File
@@ -0,0 +1,328 @@
import { CSSProperties, FC, useRef } from "react";
import style from "./scheduleView.module.css";
import { useClipboardStore } from "@store/Clipboard.Store";
import { useAlert } from "@core/Store/Alert.Store";
import Avatar from "@components/Avatar/Avatar";
/**
* Given a background color and two foreground color options, returns the
* foreground color with the best contrast ratio to the background color.
*/
export const testContrast = (
bg: CSSProperties["color"],
fg1: CSSProperties["color"],
fg2: CSSProperties["color"],
skewReturnLighter: number = 0
): CSSProperties["color"] => {
const bgLuminance = clamp(getLuminance(bg) - skewReturnLighter, 0, 1);
const fg1Luminance = getLuminance(fg1);
const fg2Luminance = getLuminance(fg2);
const fg1Contrast = getContrast(bgLuminance, fg1Luminance);
const fg2Contrast = getContrast(bgLuminance, fg2Luminance);
return fg1Contrast > fg2Contrast ? fg1 : fg2;
};
/**
* Returns the contrast ratio between two luminances
*/
export const getContrast = (bgLuminance: number, fgLuminance: number): number => {
return Math.abs(bgLuminance - fgLuminance);
};
/**
* Returns the luminance value of a CSS color
*/
export const getLuminance = (color: CSSProperties["color"]): number => {
const rgb = cssColorToRGB(color);
const r = rgb[0] / 255;
const g = rgb[1] / 255;
const b = rgb[2] / 255;
const luminance = Math.sqrt(0.299 * r ** 2 + 0.587 * g ** 2 + 0.114 * b ** 2);
return luminance;
};
/**
* Converts a CSS color string to RGB values
*/
export const cssColorToRGB = (color: CSSProperties["color"]): number[] => {
const isBrowser = typeof window !== "undefined";
if (!color || !isBrowser) return [0, 0, 0];
const canvas = document.createElement("canvas");
canvas.width = 1;
canvas.height = 1;
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("Could not get canvas context");
ctx.fillStyle = color;
ctx.fillRect(0, 0, 1, 1);
const data = ctx.getImageData(0, 0, 1, 1).data;
return [data[0], data[1], data[2]];
};
// https://github.com/30-seconds/30-seconds-of-code/blob/master/snippets/RGBToHSL.md
export const RGBToHSL = ([r, g, b]: number[]) => {
r /= 255;
g /= 255;
b /= 255;
const l = Math.max(r, g, b);
const s = l - Math.min(r, g, b);
const h = s ? (l === r ? (g - b) / s : l === g ? 2 + (b - r) / s : 4 + (r - g) / s) : 0;
return [
60 * h < 0 ? 60 * h + 360 : 60 * h,
100 * (s ? (l <= 0.5 ? s / (2 * l - s) : s / (2 - (2 * l - s))) : 0),
(100 * (2 * l - s)) / 2,
];
};
export const SUBDIVISIONS_PER_HOUR = 12;
export interface DaySchedule<CustomCalendarEvent extends CalendarEvent = CalendarEvent> {
name: string;
events: CustomCalendarEvent[];
}
export interface CalendarEvent {
startTime: number;
endTime: number;
title: string;
description?: string;
color?: CSSProperties["backgroundColor"];
}
// Rows/cols indexed starting from 1 to match CSS
export interface PositionedEvent<CustomCalendarEvent extends CalendarEvent> {
event: CustomCalendarEvent;
col: number;
endCol: number;
// Row numbers are relative to the first row of the event group
row: number;
endRow: number;
}
export interface PositionedEventGroup<CustomCalendarEvent extends CalendarEvent> {
totalCols: number;
groupStartRow: number;
groupEndRow: number;
positionedEvents: PositionedEvent<CustomCalendarEvent>[];
}
export type ScheduleThemeOverride = DeepPartial<ScheduleTheme>;
export type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
export interface ScheduleTheme<CustomCalendarEvent extends CalendarEvent = CalendarEvent> {
style?: ScheduleThemeStyle;
hourHeight: CSSProperties["height"];
minorGridlinesPerHour: number;
timeRangeFormatter: (startTime: number, endTime: number) => string;
timeFormatter: (time: number) => string;
defaultTileColor:
| CSSProperties["backgroundColor"]
| ((event: CustomCalendarEvent) => CSSProperties["backgroundColor"]);
customTileComponent?: FC<{ event: CustomCalendarEvent }>;
themeTileContent?: FC<{ event: CustomCalendarEvent }>;
}
export interface ScheduleThemeStyle<CustomCalendarEvent extends CalendarEvent = CalendarEvent> {
root?: CSSProperties;
dayLabels?: CSSProperties;
timeScaleLabels?: CSSProperties;
eventTiles?:
| CSSProperties
| ((
event: CustomCalendarEvent,
theme: ScheduleTheme<CustomCalendarEvent>
) => CSSProperties);
majorGridlinesBorder?: CSSProperties["borderStyle"];
minorGridlinesBorder?: CSSProperties["borderStyle"];
verticalGridlinesBorder?: CSSProperties["borderStyle"];
}
export const clamp = (value: number, min: number, max: number) =>
Math.min(Math.max(value, min), max);
export const numToHHMM = (num: number) => {
let hours = Math.floor(num) % 12;
if (hours === 0) {
hours = 12;
}
const minutes = Math.round((num % 1) * 60);
return `${hours}:${minutes < 10 ? "0" : ""}${minutes} ${num >= 12 && num < 24 ? "PM" : "AM"}`;
};
export const numToHH = (num: number) => {
let hours = Math.floor(num) % 12;
if (hours === 0) {
hours = 12;
}
return `${hours} ${num >= 12 && num < 24 ? "PM" : "AM"}`;
};
export const timeFormatter = (startTime: number) => `${numToHHMM(startTime)}`;
export const timeRangeFormatter = (startTime: number, endTime: number) =>
`${numToHHMM(startTime)} - ${numToHHMM(endTime)}`;
export const googleColors = {
greyGridline: "#a6a7ac",
greyTimeLabel: "#94979b",
greyDayLabel: "#666a6d",
blue: "#5186EC",
red: "#C3281C",
yellow: "#EEC04B",
purple: "#8332A4",
green: "#397D49",
indigo: "#4153AF",
pink: "#C63461",
lavender: "#AF9FD7",
orange: "#E35C33",
brown: "#74574A",
};
const calculateTextColor = (event: CalendarEvent, theme: ScheduleTheme): CSSProperties["color"] => {
const defaultColor =
typeof theme.defaultTileColor === "function"
? theme.defaultTileColor(event)
: theme.defaultTileColor;
return testContrast(event.color ?? defaultColor, "white", "black", 0.3);
};
const calculateBackgroundOpacity = (event: TXCalendarEvent): CSSProperties["opacity"] => {
return event.appointmentId ? 1 : 0.7;
};
export interface TXCalendarEvent extends CalendarEvent {
appointmentId?: string;
clientId?: string;
turnoId?: string;
employeeId?: string;
serviceId?: string;
avatar?: string;
repeatId?: string;
payment?: boolean;
}
const TurnosXpressTitle = (props: { event: TXCalendarEvent }) => {
let imageAvatar = "";
const alert = useAlert();
const setCopiedAppointment = useClipboardStore(state => state.setCopiedAppointment);
const pressTimeout = useRef<NodeJS.Timeout | null>(null);
const isLongPress = useRef(false);
if (props.event.avatar) {
imageAvatar = props.event.avatar;
}
const handleStart = (e: React.TouchEvent | React.MouseEvent) => {
isLongPress.current = false;
const target = e.currentTarget;
target.parentElement?.classList.add(style.eventRowViewAnimated);
pressTimeout.current = setTimeout(() => {
isLongPress.current = true;
target.parentElement?.classList.remove(style.eventRowViewAnimated);
if (props.event.appointmentId) {
setCopiedAppointment(props.event);
alert.showSuccess("ID del turno copiado exitosamente.");
}
}, 500);
};
const handleCancel = (e: React.TouchEvent | React.MouseEvent) => {
if (pressTimeout.current) {
clearTimeout(pressTimeout.current);
pressTimeout.current = null;
}
e.currentTarget.parentElement?.classList.remove(style.eventRowViewAnimated);
};
const handleClick = (e: React.MouseEvent) => {
if (isLongPress.current) {
e.stopPropagation();
e.preventDefault();
}
};
return (
<div
style={{ width: '100%', height: '100%' }}
onTouchStart={handleStart}
onTouchEnd={handleCancel}
onTouchCancel={handleCancel}
onTouchMove={handleCancel}
onMouseDown={handleStart}
onMouseUp={handleCancel}
onMouseLeave={handleCancel}
onClickCapture={handleClick}
>
<div className={style.turnosXpressTitleContent}>
<Avatar
src={imageAvatar}
alt={props.event.title}
size="super-small"
border="none"
style={{ minWidth: "32px" }}
/>
<h1>{props.event.title}</h1>
</div>
<div className={style.turnosXpressTimeContent}>
{timeRangeFormatter(props.event.startTime, props.event.endTime)}
{props.event.payment && (
<span style={{ marginLeft: "5px", marginTop: "5px" }}>💰</span>
)}
</div>
</div>
);
};
export const scViewTurnosExpress = (fractionTime: number = 60): ScheduleTheme<TXCalendarEvent> => {
const rowHeight = (100 * 60) / fractionTime;
return {
style: {
root: {
fontFamily: "Kanit, Roboto, Helvetica, Arial, sans-serif",
},
dayLabels: {
color: googleColors.greyDayLabel,
textTransform: "uppercase",
},
timeScaleLabels: {
color: googleColors.greyTimeLabel,
fontSize: "0.7rem",
},
majorGridlinesBorder: `1px solid #dad8d8`,
minorGridlinesBorder: `1px dotted ${googleColors.greyGridline}`,
verticalGridlinesBorder: `1x solid ${googleColors.greyGridline}`,
eventTiles: (event, theme) => ({
color: calculateTextColor(event, theme),
opacity: calculateBackgroundOpacity(event),
}),
},
hourHeight: `${rowHeight}px`,
minorGridlinesPerHour: 4,
timeRangeFormatter: timeRangeFormatter,
defaultTileColor: "var(--wine-darkest)",
timeFormatter: numToHH,
//customTileComponent: TurnosXpressTitle,
themeTileContent: TurnosXpressTitle,
};
};