diff --git a/packages/plugin-pos-api/src/graphql/schema/pos.ts b/packages/plugin-pos-api/src/graphql/schema/pos.ts index ce71bdc1f8..85689a7f60 100644 --- a/packages/plugin-pos-api/src/graphql/schema/pos.ts +++ b/packages/plugin-pos-api/src/graphql/schema/pos.ts @@ -107,6 +107,7 @@ export const types = ({ contactsEnabled, productsEnabled }) => ` posId: String code: String name: String + option: JSON } type ProductGroups { @@ -133,6 +134,7 @@ export const types = ({ contactsEnabled, productsEnabled }) => ` posId: String! code: String name: String + option: JSON } input CatProdInput { diff --git a/packages/plugin-pos-api/src/models/definitions/orders.ts b/packages/plugin-pos-api/src/models/definitions/orders.ts index 248b036ed7..6660631450 100644 --- a/packages/plugin-pos-api/src/models/definitions/orders.ts +++ b/packages/plugin-pos-api/src/models/definitions/orders.ts @@ -89,6 +89,7 @@ export interface IPosSlot { posId: string; name: string; code: string; + option: { [key: string]: number | string | boolean }; } export interface IPosSlotDocument extends IPosSlot, Document { diff --git a/packages/plugin-pos-api/src/models/definitions/pos.ts b/packages/plugin-pos-api/src/models/definitions/pos.ts index d858c80b51..12cdcb5b7d 100644 --- a/packages/plugin-pos-api/src/models/definitions/pos.ts +++ b/packages/plugin-pos-api/src/models/definitions/pos.ts @@ -62,6 +62,9 @@ export interface IPosSlot { posId: string; name: string; code: string; + options: { + [key: string]: string | number; + }; } export interface IPosSlotDocument extends IPosSlot, Document { @@ -178,7 +181,8 @@ export const posSlotSchema = schemaHooksWrapper( _id: field({ pkey: true }), name: field({ type: String, label: 'Name' }), code: field({ type: String, label: 'Code' }), - posId: field({ type: String, label: 'Pos' }) + posId: field({ type: String, label: 'Pos' }), + option: field({ type: Object, label: 'Option' }) }), 'erxes_pos_slot' ); diff --git a/packages/plugin-pos-ui/src/pos/components/Pos.tsx b/packages/plugin-pos-ui/src/pos/components/Pos.tsx index 7e9cc21ee4..6332cc54e2 100644 --- a/packages/plugin-pos-ui/src/pos/components/Pos.tsx +++ b/packages/plugin-pos-ui/src/pos/components/Pos.tsx @@ -143,7 +143,8 @@ class Pos extends React.Component { _id: m._id, code: m.code, name: m.name, - posId: m.posId + posId: m.posId, + option: m.option })); let doc: any = { diff --git a/packages/plugin-pos-ui/src/pos/components/productGroup/PosSlotItem.tsx b/packages/plugin-pos-ui/src/pos/components/productGroup/PosSlotItem.tsx index 8b6b85f81e..62ea97f61d 100644 --- a/packages/plugin-pos-ui/src/pos/components/productGroup/PosSlotItem.tsx +++ b/packages/plugin-pos-ui/src/pos/components/productGroup/PosSlotItem.tsx @@ -1,59 +1,60 @@ import React from 'react'; -import { FormGroup, Button, FormControl } from '@erxes/ui/src'; -import { Block, FlexRow } from '../../../styles'; -import { ISlot } from '../../../types'; +import { SlotListItem as StyledSlotListItem } from '../../../styles'; +import { Button, Icon, Tip, __ } from '@erxes/ui/src'; -type Props = { - onChange: (slot: ISlot) => void; - removeItem: (_id: string) => void; - slot: ISlot; -}; - -export default class PosSlotItem extends React.Component { - onChangeInput = e => { - const { slot } = this.props; - const target = e.currentTarget as HTMLInputElement; - const value = target.value; - const name = target.name; +const PosSlotItem = ({ + setActiveSlot, + _id, + name, + code, + option, + posId, + setSlots +}) => { + const deleteSlot = () => setSlots(prev => prev.filter(sl => sl._id !== _id)); - this.props.onChange({ - ...slot, - [name]: value - }); + const copySlot = () => { + const copyId = Math.random().toString(); + setSlots(prev => [ + ...(prev || []), + { + _id: copyId, + name: `${name}Copy`, + code: `${code}Copy`, + posId, + option: { + ...(option || {}), + top: option.top + 20, + left: option.left + 20 + } + } + ]); + setActiveSlot(copyId); }; + return ( + + + {code} - {name} + +
+ + + +
+
+ ); +}; - render() { - const { slot, removeItem } = this.props; - return ( - <> - - - - - - - - - + + ); +}; + +export default SlotDetail; diff --git a/packages/plugin-pos-ui/src/pos/components/productGroup/posSlotHall.tsx b/packages/plugin-pos-ui/src/pos/components/productGroup/posSlotHall.tsx new file mode 100644 index 0000000000..8d59019fb5 --- /dev/null +++ b/packages/plugin-pos-ui/src/pos/components/productGroup/posSlotHall.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Hall, HallContainer } from '../../../styles'; +import PosSlotHallItem from './posSlotHallItem'; + +const PosSlotHall = ({ + handleBack, + slots, + handleChange, + activeSlot, + setActiveSlot +}) => { + return ( + + +
+ {slots.map(sl => ( + + ))} + + + ); +}; + +export default PosSlotHall; diff --git a/packages/plugin-pos-ui/src/pos/components/productGroup/posSlotHallItem.tsx b/packages/plugin-pos-ui/src/pos/components/productGroup/posSlotHallItem.tsx new file mode 100644 index 0000000000..dd84fe9388 --- /dev/null +++ b/packages/plugin-pos-ui/src/pos/components/productGroup/posSlotHallItem.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import ResizableRect from '../slot'; + +const PosSlotHallItem = ({ + handleChange, + _id, + active, + setActiveSlot, + name, + option +}: any) => { + const handleResize = style => { + // type is a string and it shows which resize-handler you clicked + // e.g. if you clicked top-right handler, then type is 'tr' + let { top, left, width, height } = style; + top = Math.round(top); + left = Math.round(left); + width = Math.round(width); + height = Math.round(height); + handleChange( + { + top, + left, + width, + height + }, + _id + ); + }; + + const handleRotate = rotateAngle => { + handleChange( + { + rotateAngle + }, + _id + ); + }; + + const handleDrag = (deltaX, deltaY) => { + handleChange( + { + left: option.left + deltaX, + top: option.top + deltaY + }, + _id + ); + }; + + return ( + setActiveSlot(_id)} + active={active} + > + {name} + + ); +}; + +export default PosSlotHallItem; diff --git a/packages/plugin-pos-ui/src/pos/components/productGroup/posSlotPlan.tsx b/packages/plugin-pos-ui/src/pos/components/productGroup/posSlotPlan.tsx new file mode 100644 index 0000000000..c1b6a092fe --- /dev/null +++ b/packages/plugin-pos-ui/src/pos/components/productGroup/posSlotPlan.tsx @@ -0,0 +1,169 @@ +import React, { useState } from 'react'; +import { Block, BlockRow, PosSlotContainer, SlotList } from '../../../styles'; +import { + Button, + ControlLabel, + FormGroup, + ModalTrigger, + __ +} from '@erxes/ui/src'; +import PosSlotItem from './PosSlotItem'; +import PosSlotHall from './posSlotHall'; +import SlotDetail from './posSlotDetail'; + +export interface ISlot {} + +const PosSlotPlan = ({ slots: savedSlots, onSave, posId }) => { + const cleanedSlot = savedSlots.map(sl => { + const { _id, name, code, posId, option } = sl || {}; + const { + width, + height, + top, + left, + rotateAngle, + borderRadius, + color, + zIndex + } = option || {}; + + return { + _id, + name, + code, + posId, + option: { + width: width || 100, + height: height || 100, + top: top || 100, + left: left || 100, + rotateAngle: rotateAngle || 0, + borderRadius: borderRadius || 0, + color: color || '#6569DF', + zIndex: zIndex || 0 + } + }; + }); + const [slots, setSlots] = useState(cleanedSlot || []); + const [activeSlot, setActiveSlot] = useState(null); + const slotTrigger = ; + + const addSlot = () => { + const _id = Math.random().toString(); + setSlots(prev => [ + ...prev, + { + _id, + name: '', + code: '', + posId, + option: { + width: 100, + height: 100, + top: 100, + left: 100, + rotateAngle: 0, + borderRadius: 0, + color: '#673FBD', + zIndex: 0, + isShape: false + } + } + ]); + setActiveSlot(_id); + }; + + const handleBack = () => setActiveSlot(null); + + const handleChange = (property, _id, main) => + setSlots(prev => + (prev || []).map(sl => { + if (sl._id === _id) { + if (main) { + return { + ...sl, + ...property + }; + } + return { + ...sl, + option: { + ...sl.option, + ...property + } + }; + } + return sl; + }) + ); + + const cleanedSlots = slots.filter(sl => !!sl.code); + + const renderContent = props => ( + <> + + + {!!activeSlot ? ( + sl._id === activeSlot)} + handleChange={handleChange} + handleBack={handleBack} + /> + ) : ( + + {slots.map(sl => ( + + ))} +
+ + +
+
+ )} +
+ + ); + + return ( + + + + Slots: + + + + + + + ); +}; + +export default PosSlotPlan; diff --git a/packages/plugin-pos-ui/src/pos/components/slot/Rect/StyledRect.tsx b/packages/plugin-pos-ui/src/pos/components/slot/Rect/StyledRect.tsx new file mode 100644 index 0000000000..d2a29924f5 --- /dev/null +++ b/packages/plugin-pos-ui/src/pos/components/slot/Rect/StyledRect.tsx @@ -0,0 +1,172 @@ +import React from 'react'; +import styled from 'styled-components'; + +const _StyledRect: any = React.forwardRef( + (props, ref?: React.Ref) =>
+); + +const StyledRect = styled(_StyledRect)` + position: absolute; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-weight: 500; + + .square, + .resizable-handler, + .rotate { + display: none; + } + + &.rect-active { + &::before { + content: ''; + position: absolute; + top: -1px; + left: -1px; + right: -1px; + bottom: -1px; + border: 2px solid #673fbd; + } + .square, + .resizable-handler, + .rotate { + display: block; + } + + .square { + position: absolute; + width: 7px; + height: 7px; + background: white; + border-radius: 1px; + box-shadow: 0 0 4px 1px rgba(57, 76, 96, 0.15), + 0 0 0 1px rgba(43, 59, 74, 0.3); + border-radius: 3px; + &.t, + &.b { + width: 16px; + height: 6px; + } + &.tl, + &.tr, + &.bl, + &.br { + height: 12px; + width: 12px; + border-radius: 6px; + } + &.l, + &.r { + width: 6px; + height: 16px; + } + } + + .resizable-handler { + position: absolute; + width: 16px; + height: 16px; + cursor: pointer; + z-index: 1; + + &.tl, + &.t, + &.tr { + top: -8px; + } + + &.tl, + &.l, + &.bl { + left: -8px; + } + + &.bl, + &.b, + &.br { + bottom: -8px; + } + + &.br, + &.r, + &.tr { + right: -8px; + } + + &.l, + &.r { + margin-top: -8px; + } + + &.t, + &.b { + margin-left: -8px; + } + } + + .rotate { + position: absolute; + left: 50%; + top: -26px; + width: 18px; + height: 18px; + margin-left: -9px; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + } + + .t { + top: -3px; + } + + .tl, + .tr { + top: -6px; + } + + .b { + bottom: -3px; + } + + .bl, + .br { + bottom: -6px; + } + + .r { + right: -3px; + } + + .tr, + .br { + right: -6px; + } + + .l { + left: -3px; + } + + .tl, + .bl { + left: -6px; + } + + .l, + .r { + top: 50%; + margin-top: -8px; + } + + .t, + .b { + left: 50%; + margin-left: -8px; + } + } +`; + +export default StyledRect; diff --git a/packages/plugin-pos-ui/src/pos/components/slot/Rect/index.tsx b/packages/plugin-pos-ui/src/pos/components/slot/Rect/index.tsx new file mode 100644 index 0000000000..63d9d79fd2 --- /dev/null +++ b/packages/plugin-pos-ui/src/pos/components/slot/Rect/index.tsx @@ -0,0 +1,255 @@ +import React, { useState, useRef } from 'react'; +import { getLength, getAngle, getCursor } from '../utils'; +import StyledRect from './StyledRect'; + +export interface Styles { + transform: { + rotateAngle: number; + }; + position: { + centerX: number; + centerY: number; + }; + size: { + width: number; + height: number; + }; + borderRadius: number; + color: string; + zIndex: number; +} + +interface RectProps { + styles: Styles; + zoomable?: string; + rotatable: boolean; + onResizeStart?: () => void; + onResize?: ( + deltaL: number, + alpha: number, + rect: Rect, + type: string, + isShiftKey: boolean + ) => void; + onResizeEnd?: () => void; + onRotateStart?: () => void; + onRotate?: (angle: number, startAngle: number) => void; + onRotateEnd?: () => void; + onDragStart?: () => void; + onDrag?: (deltaX: number, deltaY: number) => void; + onDragEnd?: () => void; + parentRotateAngle: number; + active: boolean; +} + +interface Rect { + width: number; + height: number; + centerX: number; + centerY: number; + rotateAngle: number; +} + +const zoomableMap: { [key: string]: string } = { + n: 't', + s: 'b', + e: 'r', + w: 'l', + ne: 'tr', + nw: 'tl', + se: 'br', + sw: 'bl' +}; + +const Rect: React.FC = props => { + const elementRef = useRef(null); + + const startDrag = (e: React.MouseEvent) => { + e.persist(); + let isMouseDown = true; + const { clientX: startX, clientY: startY } = e; + if (props.onDragStart) props.onDragStart(); + const onMove = (e: MouseEvent) => { + if (!isMouseDown) return; + e.stopImmediatePropagation(); + const { clientX, clientY } = e; + const deltaX = clientX - startX; + const deltaY = clientY - startY; + if (props.onDrag) props.onDrag(deltaX, deltaY); + }; + const onUp = () => { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + if (!isMouseDown) return; + isMouseDown = false; + if (props.onDragEnd) props.onDragEnd(); + }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }; + + const startRotate = (e: React.MouseEvent) => { + if (e.button !== 0) return; + let isMouseDown = true; + const { clientX, clientY } = e; + const { + styles: { + transform: { rotateAngle: startAngle } + } + } = props; + + const rect = elementRef.current?.node?.getBoundingClientRect(); + + const { left, width, top, height } = rect || {}; + const center = { + x: (left || 0) + (width || 0) / 2, + y: (top || 0) + (height || 0) / 2 + }; + const startVector = { + x: clientX - center.x, + y: clientY - center.y + }; + + if (props.onRotateStart) props.onRotateStart(); + const onMove = (e: MouseEvent) => { + if (!isMouseDown) return; + e.stopImmediatePropagation(); + const { clientX, clientY } = e; + const rotateVector = { + x: clientX - center.x, + y: clientY - center.y + }; + const angle = getAngle(startVector, rotateVector); + if (props.onRotate) props.onRotate(angle, startAngle); + }; + const onUp = () => { + document.removeEventListener('mousemove', onMove); + document.removeEventListener('mouseup', onUp); + if (!isMouseDown) return; + isMouseDown = false; + if (props.onRotateEnd) props.onRotateEnd(); + }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }; + + const startResize = (e: React.MouseEvent, cursor: string) => { + let isMouseDown = false; + if (e.button !== 0) return; + document.body.style.cursor = cursor; + const { + styles: { + position: { centerX, centerY }, + size: { width, height }, + transform: { rotateAngle } + } + } = props; + const { clientX: startX, clientY: startY } = e; + const rect: Rect = { width, height, centerX, centerY, rotateAngle }; + const type = + (e.currentTarget && + e.currentTarget.getAttribute('class')?.split(' ')[0]) || + ''; + if (props.onResizeStart) props.onResizeStart(); + + isMouseDown = true; + + const onMove = (e: MouseEvent) => { + if (!isMouseDown) return; + e.stopImmediatePropagation(); + const { clientX, clientY } = e; + const deltaX = clientX - startX; + const deltaY = clientY - startY; + const alpha = Math.atan2(deltaY, deltaX); + const deltaL = getLength(deltaX, deltaY); + const isShiftKey = e.shiftKey; + if (props.onResize) props.onResize(deltaL, alpha, rect, type, isShiftKey); + }; + + const onUp = () => { + document.body.style.cursor = 'auto'; + document.removeEventListener('mousemove', () => setTimeout(onMove)); + document.removeEventListener('mouseup', () => setTimeout(onUp)); + if (!isMouseDown) return; + isMouseDown = false; + if (props.onResizeEnd) props.onResizeEnd(); + }; + document.addEventListener('mousemove', onMove); + document.addEventListener('mouseup', onUp); + }; + + const { + styles: { + position: { centerX, centerY }, + size: { width, height }, + transform: { rotateAngle }, + borderRadius, + color, + zIndex + }, + zoomable, + rotatable, + parentRotateAngle, + active + } = props; + + const style = { + width: Math.abs(width), + height: Math.abs(height), + transform: `rotate(${rotateAngle}deg)`, + left: centerX - Math.abs(width) / 2, + top: centerY - Math.abs(height) / 2, + borderRadius: Number(borderRadius), + background: color, + zIndex + }; + const direction = (zoomable || '') + .split(',') + .map(d => d.trim()) + .filter(d => d); + + return ( + + {rotatable && ( +
+ + + +
+ )} +
+ {props.children} +
+ + {direction.map(d => { + const cursor = `${getCursor( + rotateAngle + parentRotateAngle, + d + )}-resize`; + return ( +
startResize(e, cursor)} + /> + ); + })} + + {direction.map(d => ( +
+ ))} + + ); +}; + +export default Rect; diff --git a/packages/plugin-pos-ui/src/pos/components/slot/index.tsx b/packages/plugin-pos-ui/src/pos/components/slot/index.tsx new file mode 100644 index 0000000000..24db02ba1d --- /dev/null +++ b/packages/plugin-pos-ui/src/pos/components/slot/index.tsx @@ -0,0 +1,160 @@ +import React, { useCallback } from 'react'; +import Rect from './Rect'; +import { centerToTL, tLToCenter, getNewStyle, degToRadian } from './utils'; + +interface ResizableRectProps { + left: number; + top: number; + width: number; + height: number; + color: string; + zIndex: number; + rotatable?: boolean; + rotateAngle?: number; + parentRotateAngle?: number; + zoomable?: string; + minWidth?: number; + minHeight?: number; + aspectRatio?: number | boolean; + onRotateStart?: () => void; + onRotate?: (rotateAngle: number) => void; + onRotateEnd?: () => void; + onResizeStart?: () => void; + onResize?: (style: { + top: number; + left: number; + width: number; + height: number; + }) => void; + onResizeEnd?: () => void; + onDragStart?: () => void; + onDrag?: (deltaX: number, deltaY: number) => void; + onDragEnd?: () => void; + borderRadius?: number; + active: boolean; +} + +const ResizableRect: React.FC = ({ + top, + left, + width, + height, + color, + zIndex, + rotateAngle = 0, + borderRadius = 0, + parentRotateAngle = 0, + zoomable, + rotatable, + onRotate, + onResizeStart, + onResize, + onResizeEnd, + onRotateStart, + onRotateEnd, + onDragStart, + onDrag, + onDragEnd, + aspectRatio = 0, + minHeight = 0, + minWidth = 0, + active, + children +}) => { + const handleRotate = useCallback( + (angle: number, startAngle: number) => { + if (!onRotate) return; + let rotateAngle = Math.round(startAngle + angle); + if (rotateAngle >= 360) { + rotateAngle -= 360; + } else if (rotateAngle < 0) { + rotateAngle += 360; + } + if (rotateAngle > 356 || rotateAngle < 4) { + rotateAngle = 0; + } else if (rotateAngle > 86 && rotateAngle < 94) { + rotateAngle = 90; + } else if (rotateAngle > 176 && rotateAngle < 184) { + rotateAngle = 180; + } else if (rotateAngle > 266 && rotateAngle < 274) { + rotateAngle = 270; + } + onRotate(rotateAngle); + }, + [onRotate] + ); + + const handleDrag = useCallback( + (deltaX: number, deltaY: number) => { + if (onDrag) onDrag(deltaX, deltaY); + }, + [onDrag] + ); + + const handleResize = useCallback( + ( + length: number, + alpha: number, + rect: Rect, + type: string, + isShiftKey: boolean + ) => { + if (!onResize) return; + + const beta = alpha - degToRadian(rotateAngle + parentRotateAngle); + const deltaW = length * Math.cos(beta); + const deltaH = length * Math.sin(beta); + const ratio = + isShiftKey && !aspectRatio ? rect.width / rect.height : aspectRatio; + const { + position: { centerX, centerY }, + size: { width, height } + } = getNewStyle( + type, + { ...rect, rotateAngle }, + deltaW, + deltaH, + ratio, + minWidth, + minHeight + ); + + onResize(centerToTL({ centerX, centerY, width, height, rotateAngle })); + }, + [onResize] + ); + + const styles = tLToCenter({ + top, + left, + width, + height, + rotateAngle, + borderRadius, + color, + zIndex + }); + + return ( + + {children} + + ); +}; + +export default ResizableRect; diff --git a/packages/plugin-pos-ui/src/pos/components/slot/utils.ts b/packages/plugin-pos-ui/src/pos/components/slot/utils.ts new file mode 100644 index 0000000000..1e081c626b --- /dev/null +++ b/packages/plugin-pos-ui/src/pos/components/slot/utils.ts @@ -0,0 +1,276 @@ +export const getLength = (x, y) => Math.sqrt(x * x + y * y); + +export const getAngle = ({ x: x1, y: y1 }, { x: x2, y: y2 }) => { + const dot = x1 * x2 + y1 * y2; + const det = x1 * y2 - y1 * x2; + const angle = (Math.atan2(det, dot) / Math.PI) * 180; + return (angle + 360) % 360; +}; + +export const degToRadian = deg => (deg * Math.PI) / 180; + +const cos = deg => Math.cos(degToRadian(deg)); +const sin = deg => Math.sin(degToRadian(deg)); + +const setWidthAndDeltaW = (width, deltaW, minWidth) => { + const expectedWidth = width + deltaW; + if (expectedWidth > minWidth) { + width = expectedWidth; + } else { + deltaW = minWidth - width; + width = minWidth; + } + return { width, deltaW }; +}; + +const setHeightAndDeltaH = (height, deltaH, minHeight) => { + const expectedHeight = height + deltaH; + if (expectedHeight > minHeight) { + height = expectedHeight; + } else { + deltaH = minHeight - height; + height = minHeight; + } + return { height, deltaH }; +}; + +export const getNewStyle = ( + type, + rect, + deltaW, + deltaH, + ratio, + minWidth, + minHeight +) => { + let { width, height, centerX, centerY, rotateAngle } = rect; + const widthFlag = width < 0 ? -1 : 1; + const heightFlag = height < 0 ? -1 : 1; + width = Math.abs(width); + height = Math.abs(height); + switch (type) { + case 'r': { + const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth); + width = widthAndDeltaW.width; + deltaW = widthAndDeltaW.deltaW; + if (ratio) { + deltaH = deltaW / ratio; + height = width / ratio; + // 左上角固定 + centerX += + (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle); + centerY += + (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle); + } else { + // 左边固定 + centerX += (deltaW / 2) * cos(rotateAngle); + centerY += (deltaW / 2) * sin(rotateAngle); + } + break; + } + case 'tr': { + deltaH = -deltaH; + const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth); + width = widthAndDeltaW.width; + deltaW = widthAndDeltaW.deltaW; + const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight); + height = heightAndDeltaH.height; + deltaH = heightAndDeltaH.deltaH; + if (ratio) { + deltaW = deltaH * ratio; + width = height * ratio; + } + centerX += + (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle); + centerY += + (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle); + break; + } + case 'br': { + const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth); + width = widthAndDeltaW.width; + deltaW = widthAndDeltaW.deltaW; + const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight); + height = heightAndDeltaH.height; + deltaH = heightAndDeltaH.deltaH; + if (ratio) { + deltaW = deltaH * ratio; + width = height * ratio; + } + centerX += + (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle); + centerY += + (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle); + break; + } + case 'b': { + const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight); + height = heightAndDeltaH.height; + deltaH = heightAndDeltaH.deltaH; + if (ratio) { + deltaW = deltaH * ratio; + width = height * ratio; + // 左上角固定 + centerX += + (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle); + centerY += + (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle); + } else { + // 上边固定 + centerX -= (deltaH / 2) * sin(rotateAngle); + centerY += (deltaH / 2) * cos(rotateAngle); + } + break; + } + case 'bl': { + deltaW = -deltaW; + const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth); + width = widthAndDeltaW.width; + deltaW = widthAndDeltaW.deltaW; + const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight); + height = heightAndDeltaH.height; + deltaH = heightAndDeltaH.deltaH; + if (ratio) { + height = width / ratio; + deltaH = deltaW / ratio; + } + centerX -= + (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle); + centerY -= + (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle); + break; + } + case 'l': { + deltaW = -deltaW; + const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth); + width = widthAndDeltaW.width; + deltaW = widthAndDeltaW.deltaW; + if (ratio) { + height = width / ratio; + deltaH = deltaW / ratio; + // 右上角固定 + centerX -= + (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle); + centerY -= + (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle); + } else { + // 右边固定 + centerX -= (deltaW / 2) * cos(rotateAngle); + centerY -= (deltaW / 2) * sin(rotateAngle); + } + break; + } + case 'tl': { + deltaW = -deltaW; + deltaH = -deltaH; + const widthAndDeltaW = setWidthAndDeltaW(width, deltaW, minWidth); + width = widthAndDeltaW.width; + deltaW = widthAndDeltaW.deltaW; + const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight); + height = heightAndDeltaH.height; + deltaH = heightAndDeltaH.deltaH; + if (ratio) { + width = height * ratio; + deltaW = deltaH * ratio; + } + centerX -= + (deltaW / 2) * cos(rotateAngle) - (deltaH / 2) * sin(rotateAngle); + centerY -= + (deltaW / 2) * sin(rotateAngle) + (deltaH / 2) * cos(rotateAngle); + break; + } + case 't': { + deltaH = -deltaH; + const heightAndDeltaH = setHeightAndDeltaH(height, deltaH, minHeight); + height = heightAndDeltaH.height; + deltaH = heightAndDeltaH.deltaH; + if (ratio) { + width = height * ratio; + deltaW = deltaH * ratio; + // 左下角固定 + centerX += + (deltaW / 2) * cos(rotateAngle) + (deltaH / 2) * sin(rotateAngle); + centerY += + (deltaW / 2) * sin(rotateAngle) - (deltaH / 2) * cos(rotateAngle); + } else { + centerX += (deltaH / 2) * sin(rotateAngle); + centerY -= (deltaH / 2) * cos(rotateAngle); + } + break; + } + } + + return { + position: { + centerX, + centerY + }, + size: { + width: width * widthFlag, + height: height * heightFlag + } + }; +}; + +const cursorStartMap = { n: 0, ne: 1, e: 2, se: 3, s: 4, sw: 5, w: 6, nw: 7 }; +const cursorDirectionArray = ['n', 'ne', 'e', 'se', 's', 'sw', 'w', 'nw']; +const cursorMap = { + 0: 0, + 1: 1, + 2: 2, + 3: 2, + 4: 3, + 5: 4, + 6: 4, + 7: 5, + 8: 6, + 9: 6, + 10: 7, + 11: 8 +}; +export const getCursor = (rotateAngle, d) => { + const increment = cursorMap[Math.floor(rotateAngle / 30)]; + const index = cursorStartMap[d]; + const newIndex = (index + increment) % 8; + return cursorDirectionArray[newIndex]; +}; + +export const centerToTL = ({ + centerX, + centerY, + width, + height, + rotateAngle +}) => ({ + top: centerY - height / 2, + left: centerX - width / 2, + width, + height, + rotateAngle +}); + +export const tLToCenter = ({ + top, + left, + width, + height, + rotateAngle, + borderRadius, + color, + zIndex +}) => ({ + position: { + centerX: left + width / 2, + centerY: top + height / 2 + }, + size: { + width, + height + }, + transform: { + rotateAngle + }, + borderRadius, + color, + zIndex +}); diff --git a/packages/plugin-pos-ui/src/pos/components/step/GeneralStep.tsx b/packages/plugin-pos-ui/src/pos/components/step/GeneralStep.tsx index bd9266b8f0..a46a1698dd 100644 --- a/packages/plugin-pos-ui/src/pos/components/step/GeneralStep.tsx +++ b/packages/plugin-pos-ui/src/pos/components/step/GeneralStep.tsx @@ -1,29 +1,20 @@ import Modal from 'react-bootstrap/Modal'; import Select from 'react-select-plus'; -import PosSlotItem from '../productGroup/PosSlotItem'; import React from 'react'; import SelectDepartments from '@erxes/ui/src/team/containers/SelectDepartments'; import { __, - Button, ControlLabel, FormControl, FormGroup, - ModalTrigger, Toggle } from '@erxes/ui/src'; import { IPos, ISlot } from '../../../types'; import { LeftItem } from '@erxes/ui/src/components/step/styles'; -import { - Block, - BlockRow, - FlexColumn, - FlexItem, - FlexRow, - PosSlotAddButton -} from '../../../styles'; +import { Block, BlockRow, FlexColumn, FlexItem } from '../../../styles'; import SelectBranches from '@erxes/ui/src/team/containers/SelectBranches'; import { ALLOW_TYPES } from '../../../constants'; +import PosSlotPlan from '../productGroup/posSlotPlan'; type Props = { onChange: (name: 'pos' | 'slots' | 'allowTypes', value: any) => void; @@ -48,95 +39,6 @@ class GeneralStep extends React.Component { }; } - renderMapping(slot: ISlot, props) { - const { slots } = this.state; - - const removeItem = (_id: string) => { - const excluded = slots.filter(m => m._id !== _id); - - this.setState({ slots: excluded }); - this.props.onChange('slots', excluded); - }; - - const onChange = (changedSlot: ISlot) => { - const updated = slots.map(s => - s._id === changedSlot._id ? { ...s, ...changedSlot } : s - ); - this.setState({ slots: updated }); - this.props.onChange('slots', updated); - }; - - return ( - - ); - } - - renderPosSlotForm(trigger: React.ReactNode) { - const { slots = [] } = this.state; - - const onClickAddSlot = () => { - const m = slots.slice(); - - m.push({ - _id: Math.random().toString(), - code: '', - name: '', - posId: this.props.pos._id - }); - - this.setState({ slots: m }); - }; - - const content = props => ( - - - - - - - - - - - - - - - {slots.map(s => this.renderMapping(s, props))} - - - - - ); - - return ( - - ); - } - onChangeFunction = (name: any, value: any) => { this.props.onChange(name, value); }; @@ -287,12 +189,6 @@ class GeneralStep extends React.Component { render() { const { pos, envs } = this.props; - const slotTrigger = ( -
- Total slots: -
- ); - let name = 'POS name'; let description: any = 'description'; @@ -348,9 +244,15 @@ class GeneralStep extends React.Component { - Slots: + { + this.setState({ slots }); + this.props.onChange('slots', slots); + }} + posId={this.props.pos._id} + /> - {this.renderPosSlotForm(slotTrigger)} diff --git a/packages/plugin-pos-ui/src/pos/graphql/queries.ts b/packages/plugin-pos-ui/src/pos/graphql/queries.ts index ba3b2fb637..f73253b34d 100644 --- a/packages/plugin-pos-ui/src/pos/graphql/queries.ts +++ b/packages/plugin-pos-ui/src/pos/graphql/queries.ts @@ -102,6 +102,7 @@ const posSlots = ` posId code name + option } } `; diff --git a/packages/plugin-pos-ui/src/styles.ts b/packages/plugin-pos-ui/src/styles.ts index 1e764df916..e431af14e4 100644 --- a/packages/plugin-pos-ui/src/styles.ts +++ b/packages/plugin-pos-ui/src/styles.ts @@ -457,9 +457,67 @@ export const MenuFooter = styled.footer` max-width: 95%; `; -export const PosSlotAddButton = styled.div` - width: 100%; +export const PosSlotContainer = styled.div` + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 1rem; +`; + +export const HallContainer = styled.div` + aspect-ratio: 1; + overflow: scroll; + grid-column: span 2 / span 2; +`; + +export const Hall = styled.div` + position: relative; display: flex; - justify-content: flex-end; - margin-bottom: 10px; + width: 1000px; + height: 1000px; + background-size: 10px 10px; + background-image: linear-gradient(to right, #e5e7eb 1px, transparent 1px), + linear-gradient(to bottom, #e5e7eb 1px, transparent 1px); + overflow: hidden; + + .background { + position: absolute; + inset: 0; + } +`; + +export const SlotList = styled.div` + overflow-y: auto; + aspect-ratio: 0.5; + .slots-actions { + display: flex; + align-items: center; + gap: 0.5rem; + margin-top: 1rem; + } +`; +export const SlotListItem = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem; + padding-right: 0rem; + border-bottom: 1px solid #eee; + .actions { + display: flex; + align-items: center; + & > button { + padding: 8px !important; + } + } +`; + +export const SlotDetailStyled = styled.div` + overflow-y: auto; + aspect-ratio: 0.5; + .slot-detail-title { + padding: 0.5rem 0; + border-bottom: 1px solid #eee; + font-weight: 500; + font-size: 1.1rem; + } `; diff --git a/packages/plugin-pos-ui/src/types.ts b/packages/plugin-pos-ui/src/types.ts index 64223d9a00..94758bae92 100644 --- a/packages/plugin-pos-ui/src/types.ts +++ b/packages/plugin-pos-ui/src/types.ts @@ -82,6 +82,9 @@ export type ISlot = { code: string; name: string; posId: string; + option: { + [key: string]: string | number; + }; }; // query types diff --git a/packages/plugin-posclient-api/src/graphql/resolvers/queries/configs.ts b/packages/plugin-posclient-api/src/graphql/resolvers/queries/configs.ts index c0522ff135..b1f17efc5e 100644 --- a/packages/plugin-posclient-api/src/graphql/resolvers/queries/configs.ts +++ b/packages/plugin-posclient-api/src/graphql/resolvers/queries/configs.ts @@ -60,7 +60,7 @@ const configQueries = { if (preOrders.length) { slot.status = 'reserved'; slot.isPreDates = preOrders - .map(po => po.dueDate) + .map(po => ({ _id: po._id, dueDate: po.dueDate })) .sort((a, b) => { return ( new Date(b.dueDate).getTime() - new Date(a.dueDate).getTime() diff --git a/packages/plugin-posclient-api/src/graphql/schema/configs.ts b/packages/plugin-posclient-api/src/graphql/schema/configs.ts index 5a439bd7d5..e325cc1c2a 100644 --- a/packages/plugin-posclient-api/src/graphql/schema/configs.ts +++ b/packages/plugin-posclient-api/src/graphql/schema/configs.ts @@ -34,12 +34,18 @@ export const types = ` productId: String } + type PreDate { + _id: String + dueDate: Date + } + type PosclientSlot { _id: String code: String name: String status: String - isPreDates: [Date] + isPreDates: [PreDate] + option: JSON } type PosConfig { diff --git a/packages/plugin-posclient-api/src/models/definitions/slots.ts b/packages/plugin-posclient-api/src/models/definitions/slots.ts index 12b89a9db3..0fbb642807 100644 --- a/packages/plugin-posclient-api/src/models/definitions/slots.ts +++ b/packages/plugin-posclient-api/src/models/definitions/slots.ts @@ -6,6 +6,7 @@ export interface IPosSlot { posId: string; name: string; code: string; + option: object; } export interface IPosSlotDocument extends IPosSlot, Document { @@ -17,7 +18,8 @@ export const posSlotSchema = schemaHooksWrapper( _id: field({ pkey: true }), name: field({ type: String, label: 'Name' }), code: field({ type: String, label: 'Code' }), - posId: field({ type: String, label: 'Pos' }) + posId: field({ type: String, label: 'Pos' }), + option: field({ type: Object, lable: 'Option' }) }), 'erxes_pos_slot' ); diff --git a/pos/app/(main)/checkout/layout.tsx b/pos/app/(main)/checkout/layout.tsx index bf74da5a12..e525319dcf 100644 --- a/pos/app/(main)/checkout/layout.tsx +++ b/pos/app/(main)/checkout/layout.tsx @@ -10,7 +10,7 @@ const Layout = ({ children }: { children: React.ReactNode }) => { const mode = useAtomValue(modeAtom) return ( <> - {["main", "coffee-shop"].includes(mode) &&
{children}
} + {["main", "coffee-shop", "restaurant"].includes(mode) &&
{children}
} {mode === "kiosk" && {children}} ) diff --git a/pos/app/(main)/checkout/page.tsx b/pos/app/(main)/checkout/page.tsx index 6e5347afae..8e30df57c0 100644 --- a/pos/app/(main)/checkout/page.tsx +++ b/pos/app/(main)/checkout/page.tsx @@ -11,7 +11,7 @@ const Checkout = () => { const mode = useAtomValue(modeAtom) return ( <> - {["main", "coffee-shop"].includes(mode) &&
} + {["main", "coffee-shop", "restaurant"].includes(mode) &&
} {mode === "kiosk" && } ) diff --git a/pos/app/(main)/main.tsx b/pos/app/(main)/main.tsx index 87b58470c2..df8c142683 100644 --- a/pos/app/(main)/main.tsx +++ b/pos/app/(main)/main.tsx @@ -1,15 +1,5 @@ -import CheckoutConfig from "@/modules/auth/checkoutConfig" -import BuyAction from "@/modules/checkout/components/buyAction/buyAction.main" -import Cart from "@/modules/checkout/components/cart/cart.main" -import TotalAmount from "@/modules/checkout/components/totalAmount/totalAmount.main" -import Customer from "@/modules/customer" -import ChooseType from "@/modules/orders/components/chooseType/chooseType.main" -import OrderDetail from "@/modules/orders/OrderDetail" -import Products from "@/modules/products" -import BarcodeResult from "@/modules/products/barcodeResult.market" -import Search from "@/modules/products/components/search/search.main" -import ProductCategories from "@/modules/products/productCategories.main" -import Slots from "@/modules/slots" +import CheckoutMain from "@/modules/checkout/main" +import ProductsContainer from "@/modules/products/productsContainer" import { cn } from "@/lib/utils" import BarcodeListener from "@/components/barcodeListener" @@ -21,32 +11,10 @@ const MainIndexPage = () => {
-
- -
- -
-
-
- - - -
+
- - -
- -
- -
- - - -
-
-
+
diff --git a/pos/app/(main)/page.tsx b/pos/app/(main)/page.tsx index 5e268358ed..b3e820ebf3 100644 --- a/pos/app/(main)/page.tsx +++ b/pos/app/(main)/page.tsx @@ -7,6 +7,7 @@ import { useAtomValue } from "jotai" const Market = dynamic(() => import("./market")) const Main = dynamic(() => import("./main")) const Kiosk = dynamic(() => import("./kiosk")) +const Restaurant = dynamic(() => import("./restaurant")) export default function IndexPage() { const mode = useAtomValue(modeAtom) @@ -14,6 +15,7 @@ export default function IndexPage() { <> {mode === "market" && } {["main", "coffee-shop"].includes(mode) &&
} + {mode === "restaurant" && } {mode === "kiosk" && } ) diff --git a/pos/app/(main)/restaurant.tsx b/pos/app/(main)/restaurant.tsx new file mode 100644 index 0000000000..fced75ee80 --- /dev/null +++ b/pos/app/(main)/restaurant.tsx @@ -0,0 +1,30 @@ +import CheckoutMain from "@/modules/checkout/main" +import ProductsContainer from "@/modules/products/productsContainer" +import Slots from "@/modules/slots" +import SelectTab from "@/modules/slots/components/SelectTab" +import { selectedTabAtom } from "@/store" +import { useAtomValue } from "jotai" + +import { cn } from "@/lib/utils" +import Header from "@/components/header/header.main" + +const Restaurant = () => { + const selectedTab = useAtomValue(selectedTabAtom) + return ( + <> +
+
+
+ {selectedTab === "plan" && } + {selectedTab === "products" && } + +
+
+ +
+
+ + ) +} + +export default Restaurant diff --git a/pos/components/header/headerLayout.tsx b/pos/components/header/headerLayout.tsx index c9e83557dc..a1802f1019 100644 --- a/pos/components/header/headerLayout.tsx +++ b/pos/components/header/headerLayout.tsx @@ -21,12 +21,7 @@ const HeaderLayout = (props: { }) => { const mode = useAtomValue(modeAtom) - return ( - <> - {["coffee-shop", "main"].includes(mode) &&
} - {mode === "market" && } - - ) + return <>{mode === "market" ? :
} } export default HeaderLayout diff --git a/pos/components/header/logo.tsx b/pos/components/header/logo.tsx index ab37ee021c..816345b50c 100644 --- a/pos/components/header/logo.tsx +++ b/pos/components/header/logo.tsx @@ -1,7 +1,7 @@ "use client" import { usePathname, useRouter } from "next/navigation" -import { activeCategoryAtom } from "@/store" +import { activeCategoryAtom, slotFilterAtom } from "@/store" import { configAtom } from "@/store/config.store" import { setInitialAtom } from "@/store/order.store" import { useAtomValue, useSetAtom } from "jotai" @@ -13,6 +13,7 @@ const Logo = () => { const router = useRouter() const setInitialState = useSetAtom(setInitialAtom) const setCategory = useSetAtom(activeCategoryAtom) + const setSlotFilter = useSetAtom(slotFilterAtom) const config = useAtomValue(configAtom) const { logo } = config?.uiOptions || {} @@ -20,10 +21,11 @@ const Logo = () => { const reset = () => { setInitialState() setCategory("") + setSlotFilter(null) } return ( -
+
(pathname === "/" ? reset() : router.push("/"))} diff --git a/pos/components/ui/context-menu.tsx b/pos/components/ui/context-menu.tsx new file mode 100644 index 0000000000..38040f846b --- /dev/null +++ b/pos/components/ui/context-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as ContextMenuPrimitive from "@radix-ui/react-context-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ContextMenu = ContextMenuPrimitive.Root + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger + +const ContextMenuGroup = ContextMenuPrimitive.Group + +const ContextMenuPortal = ContextMenuPrimitive.Portal + +const ContextMenuSub = ContextMenuPrimitive.Sub + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuCheckboxItem.displayName = + ContextMenuPrimitive.CheckboxItem.displayName + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName + +const ContextMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +ContextMenuShortcut.displayName = "ContextMenuShortcut" + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, +} diff --git a/pos/modules/checkout/main.tsx b/pos/modules/checkout/main.tsx new file mode 100644 index 0000000000..6804250065 --- /dev/null +++ b/pos/modules/checkout/main.tsx @@ -0,0 +1,27 @@ +import CheckoutConfig from "@/modules/auth/checkoutConfig" +import BuyAction from "@/modules/checkout/components/buyAction/buyAction.main" +import Cart from "@/modules/checkout/components/cart/cart.main" +import TotalAmount from "@/modules/checkout/components/totalAmount/totalAmount.main" +import Customer from "@/modules/customer" +import ChooseType from "@/modules/orders/components/chooseType/chooseType.main" +import OrderDetail from "@/modules/orders/OrderDetail" + +const CheckoutMain = () => { + return ( + + +
+ +
+ +
+ + + +
+
+
+ ) +} + +export default CheckoutMain diff --git a/pos/modules/orders/ActiveOrders.main.tsx b/pos/modules/orders/ActiveOrders.main.tsx index 06ac38e2a1..814daf0145 100644 --- a/pos/modules/orders/ActiveOrders.main.tsx +++ b/pos/modules/orders/ActiveOrders.main.tsx @@ -1,5 +1,6 @@ import { useEffect } from "react" -import { activeOrderIdAtom, slotCodeAtom } from "@/store/order.store" +import { slotFilterAtom } from "@/store" +import { activeOrderIdAtom } from "@/store/order.store" import { useAtom, useAtomValue } from "jotai" import { ORDER_STATUSES } from "@/lib/constants" @@ -16,7 +17,7 @@ import useFullOrders from "./hooks/useFullOrders" const ActiveOrders = () => { const { ALL, COMPLETE } = ORDER_STATUSES const [_id, setActiveOrderId] = useAtom(activeOrderIdAtom) - const slotCode = useAtomValue(slotCodeAtom) + const slotCode = useAtomValue(slotFilterAtom) const { fullOrders, subToOrderStatuses, totalCount, handleLoadMore } = useFullOrders({ @@ -25,7 +26,7 @@ const ActiveOrders = () => { sortField: "createdAt", isPaid: false, statuses: ALL.filter((a) => a !== COMPLETE), - slotCode: _id ? undefined : slotCode, + slotCode: slotCode, }, query: queries.activeOrders, onCompleted(orders) { diff --git a/pos/modules/products/index.tsx b/pos/modules/products/index.tsx index e6ef33e6dc..7d3f9e9ab5 100644 --- a/pos/modules/products/index.tsx +++ b/pos/modules/products/index.tsx @@ -14,7 +14,7 @@ const Products = () => { return ( <> {mode === "market" && } - {mode === "main" &&
} + {["main", "restaurant"].includes(mode) &&
} {mode === "coffee-shop" && } ) diff --git a/pos/modules/products/productsContainer.tsx b/pos/modules/products/productsContainer.tsx new file mode 100644 index 0000000000..887c873ea4 --- /dev/null +++ b/pos/modules/products/productsContainer.tsx @@ -0,0 +1,29 @@ +import { modeAtom } from "@/store" +import { useAtomValue } from "jotai" + +import Products from "." +import Slots from "../slots/slots.main" +import BarcodeResult from "./barcodeResult.market" +import Search from "./components/search/search.main" +import ProductCategories from "./productCategories.main" + +const ProductsContainer = () => { + const mode = useAtomValue(modeAtom) + return ( + <> +
+ +
+ +
+
+
+ {mode !== "restaurant" && } + + +
+ + ) +} + +export default ProductsContainer diff --git a/pos/modules/settings/ChooseTheme.tsx b/pos/modules/settings/ChooseTheme.tsx index ccb788943a..5ffec18c11 100644 --- a/pos/modules/settings/ChooseTheme.tsx +++ b/pos/modules/settings/ChooseTheme.tsx @@ -30,9 +30,8 @@ const ChooseTheme = () => { Үндсэн Дэлгүүр Кофе шоп - Kiosk - Waiting Screen - Kitchen Screen + Ресторан + Киоск diff --git a/pos/modules/slots/components/SelectTab.tsx b/pos/modules/slots/components/SelectTab.tsx new file mode 100644 index 0000000000..e6b82eb85b --- /dev/null +++ b/pos/modules/slots/components/SelectTab.tsx @@ -0,0 +1,28 @@ +import { selectedTabAtom } from "@/store" +import { slotCodeAtom } from "@/store/order.store" +import { useAtom, useAtomValue } from "jotai" + +import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs" + +const SelectTab = () => { + const [selectedTab, setSelectedTab] = useAtom(selectedTabAtom) + const slot = useAtomValue(slotCodeAtom) + return ( + setSelectedTab(val as any)} + > + + + Ширээ {slot ? ` (${slot})` : ""} + + + Бараа + + + + ) +} + +export default SelectTab diff --git a/pos/modules/slots/components/createSlot.tsx b/pos/modules/slots/components/createSlot.tsx new file mode 100644 index 0000000000..742a9090ac --- /dev/null +++ b/pos/modules/slots/components/createSlot.tsx @@ -0,0 +1,29 @@ +import { selectedTabAtom } from "@/store" +import { + activeOrderIdAtom, + setInitialAtom, + slotCodeAtom, +} from "@/store/order.store" +import { useAtomValue, useSetAtom } from "jotai" + +import { ContextMenuItem } from "@/components/ui/context-menu" + +const CreateSlot = ({ code }: { code: string }) => { + const activeOrderId = useAtomValue(activeOrderIdAtom) + const setInitial = useSetAtom(setInitialAtom) + const setSlot = useSetAtom(slotCodeAtom) + const setSelectedTab = useSetAtom(selectedTabAtom) + const handleCreate = () => { + setInitial() + setSlot(code) + setSelectedTab("products") + } + + return ( + + Шинэ захиалга үүсгэх + + ) +} + +export default CreateSlot diff --git a/pos/modules/slots/components/preDates.tsx b/pos/modules/slots/components/preDates.tsx new file mode 100644 index 0000000000..f1af324e12 --- /dev/null +++ b/pos/modules/slots/components/preDates.tsx @@ -0,0 +1,39 @@ +import { selectedTabAtom } from "@/store" +import { activeOrderIdAtom } from "@/store/order.store" +import { format } from "date-fns" +import { useSetAtom } from "jotai" +import { CalendarDaysIcon } from "lucide-react" + +import { ISlot } from "@/types/slots.type" +import { + ContextMenuItem, + ContextMenuSeparator, +} from "@/components/ui/context-menu" +import { DropdownMenuLabel } from "@/components/ui/dropdown-menu" + +const PreDates = ({ isPreDates }: { isPreDates: ISlot["isPreDates"] }) => { + const setActiveOrder = useSetAtom(activeOrderIdAtom) + const setSelectedTab = useSetAtom(selectedTabAtom) + if (!isPreDates?.length) return null + return ( + <> + + Урьдчилсан захиалгууд + {isPreDates.map((preDate) => ( + { + setActiveOrder(preDate._id) + setSelectedTab("products") + }} + > + + {format(new Date(preDate.dueDate), "yyyy/MM/dd HH:mm")} + + ))} + + ) +} + +export default PreDates diff --git a/pos/modules/slots/components/Slot.tsx b/pos/modules/slots/components/slot.main.tsx similarity index 78% rename from pos/modules/slots/components/Slot.tsx rename to pos/modules/slots/components/slot.main.tsx index 19eee47e24..661da4a1de 100644 --- a/pos/modules/slots/components/Slot.tsx +++ b/pos/modules/slots/components/slot.main.tsx @@ -5,13 +5,11 @@ import { Check } from "lucide-react" import { ISlot } from "@/types/slots.type" import { Button } from "@/components/ui/button" -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from "@/components/ui/hover-card" +import { ContextMenuTrigger } from "@/components/ui/context-menu" import { Label } from "@/components/ui/label" +import SlotActions from "./slotActions" + const className = cva("h-10 w-10 text-base font-bold px-0 relative ", { variants: { status: { @@ -27,19 +25,17 @@ const className = cva("h-10 w-10 text-base font-bold px-0 relative ", { const MotionLabel = motion(Label) -const Slot = ({ - code, - name, - status, - active, - isPreDates, -}: ISlot & { - status?: "serving" | "available" | "reserved" - active: boolean -}) => { +const Slot = ( + props: ISlot & { + status?: "serving" | "available" | "reserved" + active: boolean + } +) => { + const { code, name, status, active } = props + return ( - - + + - - {isPreDates && ( - - {isPreDates.toString()} - - )} - + + ) } diff --git a/pos/modules/slots/components/slot.tsx b/pos/modules/slots/components/slot.tsx new file mode 100644 index 0000000000..87e095cf99 --- /dev/null +++ b/pos/modules/slots/components/slot.tsx @@ -0,0 +1,112 @@ +import { memo } from "react" +import { selectedTabAtom } from "@/store" +import { slotCodeAtom } from "@/store/order.store" +import { motion } from "framer-motion" +import { useSetAtom } from "jotai" +import { CheckCircle2, Circle, XCircleIcon } from "lucide-react" + +import { ISlot } from "@/types/slots.type" +import { cn } from "@/lib/utils" +import { ContextMenuTrigger } from "@/components/ui/context-menu" +import { Label } from "@/components/ui/label" + +import SlotActions from "./slotActions" + +const MotionLabel = motion(Label) + +const statusIcons = { + serving: CheckCircle2, + available: Circle, + reserved: XCircleIcon, +} + +const Slot = ( + props: ISlot & { + status?: "serving" | "available" | "reserved" + active: boolean + } +) => { + const { active, code, name, option, status } = props + + const { + rotateAngle, + width, + height, + top, + left, + color, + zIndex, + borderRadius, + isShape, + } = option || {} + + const Icon = statusIcons[status || "available"] + const style = { + width, + height, + top, + left, + transform: `rotate(${rotateAngle}deg)`, + backgroundColor: color, + zIndex, + borderRadius, + } + const setActiveSlot = useSetAtom(slotCodeAtom) + const setSelectedTab = useSetAtom(selectedTabAtom) + + if (isShape) + return ( +
+ ) + + const handleChoose = () => { + setActiveSlot(code) + setSelectedTab("products") + } + + return ( + + +
+ + {name || code} +
+ +
+
+ ) +} + +export default memo(Slot) diff --git a/pos/modules/slots/components/slotActions.tsx b/pos/modules/slots/components/slotActions.tsx new file mode 100644 index 0000000000..2e52d42fc0 --- /dev/null +++ b/pos/modules/slots/components/slotActions.tsx @@ -0,0 +1,73 @@ +import { memo } from "react" +import { selectedTabAtom, slotFilterAtom } from "@/store" +import { slotCodeAtom } from "@/store/order.store" +import { useSetAtom } from "jotai" +import { CheckCircle2, Circle, ListFilterIcon, XCircleIcon } from "lucide-react" + +import { ISlot } from "@/types/slots.type" +import { + ContextMenu, + ContextMenuContent, + ContextMenuItem, + ContextMenuSeparator, +} from "@/components/ui/context-menu" + +import CreateSlot from "./createSlot" +import PreDates from "./preDates" + +const statusIcons = { + serving: CheckCircle2, + available: Circle, + reserved: XCircleIcon, +} + +const SlotActions = ({ + code, + name, + isPreDates, + status, + children, +}: ISlot & { + status?: "serving" | "available" | "reserved" + active: boolean + children: React.ReactNode +}) => { + const setActiveSlot = useSetAtom(slotCodeAtom) + const setSelectedTab = useSetAtom(selectedTabAtom) + const setFilterSlots = useSetAtom(slotFilterAtom) + const Icon = statusIcons[status || "available"] + const handleChoose = () => { + setActiveSlot(code) + setSelectedTab("products") + } + return ( + + {children} + +
+
+ {name} ({code}) +
+
+ + {status} +
+
+ + + Сонгох + + setFilterSlots(code)} + > + + Захиалгууд + + +
+
+ ) +} + +export default memo(SlotActions) diff --git a/pos/modules/slots/graphql/queries.ts b/pos/modules/slots/graphql/queries.ts index 325cf56ff4..95853680bc 100644 --- a/pos/modules/slots/graphql/queries.ts +++ b/pos/modules/slots/graphql/queries.ts @@ -1,11 +1,17 @@ -const slots = ` - query poscSlots { - poscSlots{ +import { gql } from "@apollo/client" + +const slots = gql` + query PoscSlots { + poscSlots { _id code name + option status - isPreDates + isPreDates { + dueDate + _id + } } } ` diff --git a/pos/modules/slots/hooks/useSlots.tsx b/pos/modules/slots/hooks/useSlots.tsx index d5278d9cb3..0b03ccb300 100644 --- a/pos/modules/slots/hooks/useSlots.tsx +++ b/pos/modules/slots/hooks/useSlots.tsx @@ -1,9 +1,9 @@ -import { gql, useQuery } from "@apollo/client" +import { useQuery } from "@apollo/client" import { queries } from "../graphql" const useSlots = () => { - const { data, loading } = useQuery(gql(queries.slots)) + const { data, loading } = useQuery(queries.slots) const { poscSlots: slots } = data || {} return { slots, loading } } diff --git a/pos/modules/slots/index.tsx b/pos/modules/slots/index.tsx index 437894b022..a1c1212bdb 100644 --- a/pos/modules/slots/index.tsx +++ b/pos/modules/slots/index.tsx @@ -1,43 +1,40 @@ +import { slotFilterAtom } from "@/store" import { slotCodeAtom } from "@/store/order.store" -import { useAtom } from "jotai" +import { useAtom, useSetAtom } from "jotai" import { ISlot } from "@/types/slots.type" -import { LoaderIcon } from "@/components/ui/loader" -import { RadioGroup } from "@/components/ui/radio-group" -import { ScrollArea } from "@/components/ui/scroll-area" -import Slot from "./components/Slot" +import Slot from "./components/slot" import useSlots from "./hooks/useSlots" const Slots = () => { const { slots, loading } = useSlots() const [activeSlot, setActiveSlot] = useAtom(slotCodeAtom) - - if (!loading && !slots.length) return null - + const setSlotFilter = useSetAtom(slotFilterAtom) return ( - - setActiveSlot(value)} +
+
- {loading ? ( - - ) : ( - <> - {(slots || []).map((slot: ISlot) => ( - - ))} - - )} - - +
{ + setActiveSlot(null) + setSlotFilter(null) + }} + /> + {(slots || []).map((slot: ISlot) => ( + + ))} +
+
) } -// productsConfigs + export default Slots diff --git a/pos/modules/slots/slots.main.tsx b/pos/modules/slots/slots.main.tsx new file mode 100644 index 0000000000..712104aed6 --- /dev/null +++ b/pos/modules/slots/slots.main.tsx @@ -0,0 +1,47 @@ +import { slotFilterAtom } from "@/store" +import { slotCodeAtom } from "@/store/order.store" +import { useAtom, useSetAtom } from "jotai" + +import { ISlot } from "@/types/slots.type" +import { LoaderIcon } from "@/components/ui/loader" +import { RadioGroup } from "@/components/ui/radio-group" +import { ScrollArea } from "@/components/ui/scroll-area" + +import Slot from "./components/slot.main" +import useSlots from "./hooks/useSlots" + +const Slots = () => { + const { slots, loading } = useSlots() + const [activeSlot, setActiveSlot] = useAtom(slotCodeAtom) + const setSlotFilter = useSetAtom(slotFilterAtom) + + if (!loading && !slots.length) return null + + return ( + + { + setActiveSlot(value) + setSlotFilter(value) + }} + > + {loading ? ( + + ) : ( + <> + {(slots || []).map((slot: ISlot) => ( + + ))} + + )} + + + ) +} +export default Slots diff --git a/pos/package.json b/pos/package.json index 3cc62f8b7d..77a6df83c1 100644 --- a/pos/package.json +++ b/pos/package.json @@ -20,6 +20,7 @@ "@radix-ui/react-aspect-ratio": "^1.0.3", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-collapsible": "^1.0.3", + "@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-dialog": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-hover-card": "^1.0.7", diff --git a/pos/public/js/env.js b/pos/public/js/env.js index 8e605ef564..5162560bf4 100644 --- a/pos/public/js/env.js +++ b/pos/public/js/env.js @@ -1,6 +1,6 @@ window.env = { - NEXT_PUBLIC_MAIN_API_DOMAIN: "https://staging.erxes.io/gateway", - NEXT_PUBLIC_MAIN_SUBS_DOMAIN: "wss://staging.erxes.io/gateway/graphql", + NEXT_PUBLIC_MAIN_API_DOMAIN: "http://localhost:4000", + NEXT_PUBLIC_MAIN_SUBS_DOMAIN: "ws://localhost:4000/graphql", NEXT_PUBLIC_SERVER_API_DOMAIN: "http://localhost:4000", - NEXT_PUBLIC_SERVER_DOMAIN: "http://localhost:3000", + NEXT_PUBLIC_SERVER_DOMAIN: "http://localhost:4000", } diff --git a/pos/store/index.tsx b/pos/store/index.tsx index e693303205..f1022cb4bc 100644 --- a/pos/store/index.tsx +++ b/pos/store/index.tsx @@ -27,6 +27,10 @@ export const reportDateAtom = atom(null) export const productCountAtom = atom(0) +export const selectedTabAtom = atom<"plan" | "products">("plan") + +export const slotFilterAtom = atom(null) + // dialog export const kioskModalView = atom("") diff --git a/pos/store/order.store.ts b/pos/store/order.store.ts index e2ae000e16..915bfa7251 100644 --- a/pos/store/order.store.ts +++ b/pos/store/order.store.ts @@ -15,6 +15,7 @@ import { import { customerSearchAtom } from "." import { cartAtom, orderItemInput, totalAmountAtom } from "./cart.store" import { allowTypesAtom } from "./config.store" +import { selectedTabAtom } from "." import { paymentSheetAtom } from "./ui.store" // order @@ -89,6 +90,7 @@ export const setInitialAtom = atom( set(dueDateAtom, undefined) set(isPreAtom, undefined) set(buttonTypeAtom, null) + set(selectedTabAtom, "plan") } ) diff --git a/pos/types/config.types.ts b/pos/types/config.types.ts index b1822ce166..c76da5b02f 100644 --- a/pos/types/config.types.ts +++ b/pos/types/config.types.ts @@ -87,4 +87,4 @@ export interface ICurrentUser { } } -export type modeT = "market" | "main" | "kiosk" | "coffee-shop" +export type modeT = "market" | "main" | "kiosk" | "coffee-shop" | "restaurant" diff --git a/pos/types/slots.type.ts b/pos/types/slots.type.ts index 1c42f6dcb6..edcd25438a 100644 --- a/pos/types/slots.type.ts +++ b/pos/types/slots.type.ts @@ -2,5 +2,16 @@ export interface ISlot { _id: string code: string name: string - isPreDates: string | null + isPreDates: { dueDate: string; _id: string }[] + option: { + width: number + height: number + top: number + left: number + rotateAngle: number + borderRadius: number + color: string + zIndex: string + isShape: boolean + } } diff --git a/pos/yarn.lock b/pos/yarn.lock index a9af070261..58ee67b1c5 100644 --- a/pos/yarn.lock +++ b/pos/yarn.lock @@ -587,6 +587,19 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-context-menu@^2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context-menu/-/react-context-menu-2.1.5.tgz#1bdbd72761439f9166f75dc4598f276265785c83" + integrity sha512-R5XaDj06Xul1KGb+WP8qiOh7tKJNz2durpLBXAGZjSVtctcRFCuEvy2gtMwRJGePwQQE5nV77gs4FwRi8T+r2g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-menu" "2.0.6" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-context@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0" @@ -734,6 +747,16 @@ "@radix-ui/react-primitive" "1.0.3" "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-focus-scope@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" + integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-hover-card@^1.0.7": version "1.0.7" resolved "https://registry.yarnpkg.com/@radix-ui/react-hover-card/-/react-hover-card-1.0.7.tgz#684bca2504432566357e7157e087051aa3577948" @@ -799,6 +822,31 @@ aria-hidden "^1.1.1" react-remove-scroll "2.5.5" +"@radix-ui/react-menu@2.0.6": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@radix-ui/react-menu/-/react-menu-2.0.6.tgz#2c9e093c1a5d5daa87304b2a2f884e32288ae79e" + integrity sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-popper" "1.1.3" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-callback-ref" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + "@radix-ui/react-navigation-menu@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.3.tgz#4b0f078226971b56b0fe2a9e4dde3cc2abfa731f"