@obisoft/ventas-ui 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/package.json +48 -0
- package/src/checkout/CheckoutDrawerFrame.tsx +82 -0
- package/src/checkout/CheckoutPickerButton.tsx +31 -0
- package/src/checkout/CheckoutSelectList.tsx +70 -0
- package/src/checkout/CheckoutSummary.tsx +33 -0
- package/src/checkout/PaymentAmountRow.tsx +45 -0
- package/src/checkout/index.ts +5 -0
- package/src/chrome/ActionChipBar.tsx +20 -0
- package/src/chrome/DetailRowsCard.tsx +31 -0
- package/src/chrome/SectionBlock.tsx +20 -0
- package/src/chrome/index.ts +3 -0
- package/src/dashboard/ActivityList.tsx +41 -0
- package/src/dashboard/CompanyStatusGrid.tsx +34 -0
- package/src/dashboard/DashboardHeader.tsx +41 -0
- package/src/dashboard/DashboardSection.tsx +15 -0
- package/src/dashboard/QuickActionGrid.tsx +59 -0
- package/src/index.ts +10 -0
- package/src/pos/PosCategoryChips.tsx +76 -0
- package/src/pos/PosCategoryTile.tsx +42 -0
- package/src/pos/PosDock.tsx +30 -0
- package/src/pos/PosProductTile.tsx +124 -0
- package/src/pos/PosTabBar.tsx +67 -0
- package/src/pos/PosTicket.tsx +150 -0
- package/src/pos/QuantityStepper.tsx +60 -0
- package/src/pos/index.ts +7 -0
- package/src/styles/useVentasStyles.ts +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# @obisoft/ventas-ui
|
|
2
|
+
|
|
3
|
+
Componentes presentacionales del dominio Ventas, construidos exclusivamente sobre `@obisoft/ui`.
|
|
4
|
+
|
|
5
|
+
## Estado
|
|
6
|
+
|
|
7
|
+
Versión: **`0.7.0`** (peer `@obisoft/ui >= 0.8.0`). Paquete npm público (`access: public`).
|
|
8
|
+
|
|
9
|
+
- Monorepo: `file:packages/obisoft-ventas-ui`
|
|
10
|
+
- Externo: `npm i @obisoft/ventas-ui@0.7.0` (con `NPM_TOKEN`)
|
|
11
|
+
|
|
12
|
+
Ver [docs/OBISOFT_UI_PUBLISH.md](../../docs/OBISOFT_UI_PUBLISH.md).
|
|
13
|
+
|
|
14
|
+
## Módulos
|
|
15
|
+
|
|
16
|
+
- **Dashboard** — secciones, quick actions, activity, company status
|
|
17
|
+
- **POS** — tiles, dock, tabs, ticket, quantity stepper
|
|
18
|
+
- **Checkout** — drawer frame, summary, payment rows, select lists
|
|
19
|
+
- **Chrome** — `SectionBlock`, `DetailRowsCard`, `ActionChipBar`
|
|
20
|
+
- **Styles** — `useVentasStyles`
|
|
21
|
+
|
|
22
|
+
## Storybook
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm run storybook
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Regla
|
|
29
|
+
|
|
30
|
+
Solo puede depender de `@obisoft/ui` y peers de React Native. Nunca importa `src/app`, `features`, `entities` ni `pages`. Validar con `npm run audit:ui`.
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@obisoft/ventas-ui",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"description": "Presentational React Native components for Obisoft Ventas built on @obisoft/ui.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./src/index.ts",
|
|
8
|
+
"react-native": "./src/index.ts",
|
|
9
|
+
"types": "./src/index.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"react-native": "./src/index.ts",
|
|
13
|
+
"types": "./src/index.ts",
|
|
14
|
+
"default": "./src/index.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"react-native",
|
|
23
|
+
"obisoft",
|
|
24
|
+
"ventas",
|
|
25
|
+
"ui"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"registry": "https://registry.npmjs.org/"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/obisoft/obi-projects.git",
|
|
34
|
+
"directory": "obi-ventas-mobile/packages/obisoft-ventas-ui"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/obisoft/obi-projects/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/obisoft/obi-projects/tree/main/obi-ventas-mobile/packages/obisoft-ventas-ui#readme",
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"react": ">=19.0.0",
|
|
42
|
+
"react-native": ">=0.80.0",
|
|
43
|
+
"@obisoft/ui": ">=0.8.0",
|
|
44
|
+
"react-native-safe-area-context": ">=5.0.0",
|
|
45
|
+
"react-native-gesture-handler": ">=3.0.0",
|
|
46
|
+
"react-native-linear-gradient": ">=2.8.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React, { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { Animated, Keyboard, Modal, Pressable, useWindowDimensions, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
|
4
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
|
+
import { useObiTheme } from '@obisoft/ui';
|
|
6
|
+
|
|
7
|
+
export function CheckoutDrawerFrame({
|
|
8
|
+
visible,
|
|
9
|
+
onClose,
|
|
10
|
+
header,
|
|
11
|
+
children,
|
|
12
|
+
footer,
|
|
13
|
+
maxWidth = 420,
|
|
14
|
+
widthRatio = 0.88,
|
|
15
|
+
bodyStyle,
|
|
16
|
+
}: {
|
|
17
|
+
visible: boolean;
|
|
18
|
+
onClose: () => void;
|
|
19
|
+
header?: React.ReactNode;
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
footer?: React.ReactNode;
|
|
22
|
+
maxWidth?: number;
|
|
23
|
+
widthRatio?: number;
|
|
24
|
+
bodyStyle?: StyleProp<ViewStyle>;
|
|
25
|
+
}) {
|
|
26
|
+
const theme = useObiTheme();
|
|
27
|
+
const insets = useSafeAreaInsets();
|
|
28
|
+
const { width } = useWindowDimensions();
|
|
29
|
+
const panelWidth = useMemo(() => Math.min(maxWidth, Math.round(width * widthRatio)), [maxWidth, width, widthRatio]);
|
|
30
|
+
const slide = useRef(new Animated.Value(panelWidth)).current;
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!visible) {
|
|
34
|
+
slide.setValue(panelWidth);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
slide.setValue(panelWidth);
|
|
38
|
+
Animated.timing(slide, { toValue: 0, duration: 220, useNativeDriver: true }).start();
|
|
39
|
+
}, [panelWidth, slide, visible]);
|
|
40
|
+
|
|
41
|
+
const closeAnimated = () => {
|
|
42
|
+
Keyboard.dismiss();
|
|
43
|
+
Animated.timing(slide, { toValue: panelWidth, duration: 180, useNativeDriver: true }).start(({ finished }) => {
|
|
44
|
+
if (finished) onClose();
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Modal visible={visible} transparent animationType="none" onRequestClose={closeAnimated} statusBarTranslucent>
|
|
50
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
51
|
+
<View style={{ flex: 1, flexDirection: 'row', justifyContent: 'flex-end', backgroundColor: theme.glass.backdropStrong }}>
|
|
52
|
+
<Pressable style={{ flex: 1 }} onPress={closeAnimated} accessibilityLabel="Cerrar cobro" />
|
|
53
|
+
<Animated.View style={{ width: panelWidth, height: '100%', transform: [{ translateX: slide }] }}>
|
|
54
|
+
<View
|
|
55
|
+
style={{
|
|
56
|
+
flex: 1,
|
|
57
|
+
paddingTop: insets.top,
|
|
58
|
+
paddingBottom: insets.bottom,
|
|
59
|
+
backgroundColor: theme.glass.panel,
|
|
60
|
+
borderTopLeftRadius: theme.radius.lg,
|
|
61
|
+
borderBottomLeftRadius: theme.radius.lg,
|
|
62
|
+
overflow: 'hidden',
|
|
63
|
+
}}
|
|
64
|
+
>
|
|
65
|
+
{header ? (
|
|
66
|
+
<View style={{ paddingHorizontal: theme.spacing.lg, paddingTop: theme.spacing.lg, paddingBottom: theme.spacing.md, gap: theme.spacing.sm }}>
|
|
67
|
+
{header}
|
|
68
|
+
</View>
|
|
69
|
+
) : null}
|
|
70
|
+
<View style={[{ flex: 1 }, bodyStyle]}>{children}</View>
|
|
71
|
+
{footer ? (
|
|
72
|
+
<View style={{ paddingHorizontal: theme.spacing.lg, paddingTop: theme.spacing.md, paddingBottom: theme.spacing.lg, gap: theme.spacing.sm }}>
|
|
73
|
+
{footer}
|
|
74
|
+
</View>
|
|
75
|
+
) : null}
|
|
76
|
+
</View>
|
|
77
|
+
</Animated.View>
|
|
78
|
+
</View>
|
|
79
|
+
</GestureHandlerRootView>
|
|
80
|
+
</Modal>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable } from 'react-native';
|
|
3
|
+
import { AppText, Icon, shadow, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export function CheckoutPickerButton({ label, onPress, open }: { label: string; onPress: () => void; open?: boolean }) {
|
|
6
|
+
const theme = useObiTheme();
|
|
7
|
+
return (
|
|
8
|
+
<Pressable
|
|
9
|
+
accessibilityRole="button"
|
|
10
|
+
accessibilityState={{ expanded: Boolean(open) }}
|
|
11
|
+
onPress={onPress}
|
|
12
|
+
style={({ pressed }) => ({
|
|
13
|
+
minHeight: 48,
|
|
14
|
+
borderWidth: 1,
|
|
15
|
+
borderColor: open ? theme.colors.primary : theme.glass.border,
|
|
16
|
+
borderRadius: theme.radius.md,
|
|
17
|
+
backgroundColor: theme.glass.input,
|
|
18
|
+
paddingHorizontal: theme.spacing.md,
|
|
19
|
+
flexDirection: 'row',
|
|
20
|
+
alignItems: 'center',
|
|
21
|
+
justifyContent: 'space-between',
|
|
22
|
+
gap: theme.spacing.sm,
|
|
23
|
+
opacity: pressed ? 0.76 : 1,
|
|
24
|
+
...shadow.glassSm,
|
|
25
|
+
})}
|
|
26
|
+
>
|
|
27
|
+
<AppText numberOfLines={1} style={{ flex: 1 }}>{label}</AppText>
|
|
28
|
+
<Icon name={open ? 'chevron-up' : 'chevron-down'} size={18} color={theme.colors.textMuted} />
|
|
29
|
+
</Pressable>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, ScrollView, View } from 'react-native';
|
|
3
|
+
import { AppText, shadow, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export type CheckoutSelectOption = {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
subtitle?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function CheckoutSelectList({
|
|
12
|
+
items,
|
|
13
|
+
selectedKey,
|
|
14
|
+
onSelect,
|
|
15
|
+
maxHeight = 180,
|
|
16
|
+
emptyLabel = 'Sin resultados',
|
|
17
|
+
}: {
|
|
18
|
+
items: CheckoutSelectOption[];
|
|
19
|
+
selectedKey?: string | null;
|
|
20
|
+
onSelect: (key: string) => void;
|
|
21
|
+
maxHeight?: number;
|
|
22
|
+
emptyLabel?: string;
|
|
23
|
+
}) {
|
|
24
|
+
const theme = useObiTheme();
|
|
25
|
+
return (
|
|
26
|
+
<View
|
|
27
|
+
style={{
|
|
28
|
+
maxHeight,
|
|
29
|
+
borderWidth: 1,
|
|
30
|
+
borderColor: theme.glass.border,
|
|
31
|
+
borderRadius: theme.radius.md,
|
|
32
|
+
overflow: 'hidden',
|
|
33
|
+
backgroundColor: theme.glass.surface,
|
|
34
|
+
...shadow.glassSm,
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<ScrollView nestedScrollEnabled keyboardShouldPersistTaps="handled">
|
|
38
|
+
{items.length === 0 ? (
|
|
39
|
+
<View style={{ paddingHorizontal: theme.spacing.md, paddingVertical: theme.spacing.lg }}>
|
|
40
|
+
<AppText muted>{emptyLabel}</AppText>
|
|
41
|
+
</View>
|
|
42
|
+
) : null}
|
|
43
|
+
{items.map((item, index) => {
|
|
44
|
+
const selected = item.key === selectedKey;
|
|
45
|
+
return (
|
|
46
|
+
<Pressable
|
|
47
|
+
key={item.key}
|
|
48
|
+
onPress={() => onSelect(item.key)}
|
|
49
|
+
style={({ pressed }) => ({
|
|
50
|
+
paddingHorizontal: theme.spacing.md,
|
|
51
|
+
paddingVertical: theme.spacing.sm,
|
|
52
|
+
borderBottomWidth: index === items.length - 1 ? 0 : 1,
|
|
53
|
+
borderBottomColor: theme.glass.separator,
|
|
54
|
+
backgroundColor: selected
|
|
55
|
+
? theme.glass.primaryTint
|
|
56
|
+
: pressed
|
|
57
|
+
? theme.glass.controlPressed
|
|
58
|
+
: 'transparent',
|
|
59
|
+
gap: 2,
|
|
60
|
+
})}
|
|
61
|
+
>
|
|
62
|
+
<AppText weight="700" numberOfLines={1}>{item.label}</AppText>
|
|
63
|
+
{item.subtitle ? <AppText variant="caption" muted numberOfLines={1}>{item.subtitle}</AppText> : null}
|
|
64
|
+
</Pressable>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</ScrollView>
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { AppText, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export type CheckoutSummaryItem = {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
value: string;
|
|
9
|
+
tone?: 'default' | 'warning' | 'success' | 'danger';
|
|
10
|
+
strong?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function CheckoutSummary({ items }: { items: CheckoutSummaryItem[] }) {
|
|
14
|
+
const theme = useObiTheme();
|
|
15
|
+
const toneColor = (tone: CheckoutSummaryItem['tone']) => {
|
|
16
|
+
if (tone === 'warning') return theme.colors.warning;
|
|
17
|
+
if (tone === 'success') return theme.colors.success;
|
|
18
|
+
if (tone === 'danger') return theme.colors.danger;
|
|
19
|
+
return theme.colors.text;
|
|
20
|
+
};
|
|
21
|
+
return (
|
|
22
|
+
<View style={{ gap: theme.spacing.sm }}>
|
|
23
|
+
{items.map(item => (
|
|
24
|
+
<View key={item.key} style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: theme.spacing.md }}>
|
|
25
|
+
<AppText muted={item.tone == null || item.tone === 'default'} style={{ color: item.tone && item.tone !== 'default' ? toneColor(item.tone) : undefined }}>
|
|
26
|
+
{item.label}
|
|
27
|
+
</AppText>
|
|
28
|
+
<AppText weight={item.strong ? '900' : '800'} style={{ color: toneColor(item.tone) }}>{item.value}</AppText>
|
|
29
|
+
</View>
|
|
30
|
+
))}
|
|
31
|
+
</View>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { AppText, TextField, shadow, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export function PaymentAmountRow({
|
|
6
|
+
label,
|
|
7
|
+
value,
|
|
8
|
+
placeholder = '0.00',
|
|
9
|
+
onChangeText,
|
|
10
|
+
}: {
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
placeholder?: string;
|
|
14
|
+
onChangeText: (value: string) => void;
|
|
15
|
+
}) {
|
|
16
|
+
const theme = useObiTheme();
|
|
17
|
+
return (
|
|
18
|
+
<View
|
|
19
|
+
style={{
|
|
20
|
+
flexDirection: 'row',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
gap: theme.spacing.md,
|
|
23
|
+
minHeight: 48,
|
|
24
|
+
paddingHorizontal: theme.spacing.md,
|
|
25
|
+
paddingVertical: theme.spacing.sm,
|
|
26
|
+
borderRadius: theme.radius.md,
|
|
27
|
+
backgroundColor: theme.glass.surface,
|
|
28
|
+
borderWidth: 1,
|
|
29
|
+
borderColor: theme.glass.border,
|
|
30
|
+
...shadow.glassSm,
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<AppText weight="800" style={{ flex: 1 }} numberOfLines={1}>{label}</AppText>
|
|
34
|
+
<View style={{ width: 132, flexShrink: 0 }}>
|
|
35
|
+
<TextField
|
|
36
|
+
keyboardType="decimal-pad"
|
|
37
|
+
value={value}
|
|
38
|
+
placeholder={placeholder}
|
|
39
|
+
onChangeText={onChangeText}
|
|
40
|
+
style={{ width: '100%', minHeight: 40, margin: 0, paddingHorizontal: theme.spacing.sm, textAlign: 'right', fontWeight: '700' }}
|
|
41
|
+
/>
|
|
42
|
+
</View>
|
|
43
|
+
</View>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CheckoutDrawerFrame } from './CheckoutDrawerFrame';
|
|
2
|
+
export { CheckoutSelectList, type CheckoutSelectOption } from './CheckoutSelectList';
|
|
3
|
+
export { CheckoutPickerButton } from './CheckoutPickerButton';
|
|
4
|
+
export { PaymentAmountRow } from './PaymentAmountRow';
|
|
5
|
+
export { CheckoutSummary, type CheckoutSummaryItem } from './CheckoutSummary';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { useObiStyles } from '@obisoft/ui';
|
|
4
|
+
import { SectionBlock } from './SectionBlock';
|
|
5
|
+
|
|
6
|
+
export type ActionChipBarProps = {
|
|
7
|
+
title?: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
style?: StyleProp<ViewStyle>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Wrap action button groups (export / PDF / print) under an optional section title. */
|
|
13
|
+
export function ActionChipBar({ title = 'Acciones', children, style }: ActionChipBarProps) {
|
|
14
|
+
const ui = useObiStyles();
|
|
15
|
+
return (
|
|
16
|
+
<SectionBlock title={title} style={style}>
|
|
17
|
+
<View style={ui.chipWrap}>{children}</View>
|
|
18
|
+
</SectionBlock>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { AppText, Card, useObiStyles } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export type DetailAmountRow = {
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
strong?: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type DetailRowsCardProps = {
|
|
12
|
+
rows: DetailAmountRow[];
|
|
13
|
+
style?: StyleProp<ViewStyle>;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** Card of label/value rows used in cash reconciliation and similar summaries. */
|
|
17
|
+
export function DetailRowsCard({ rows, style }: DetailRowsCardProps) {
|
|
18
|
+
const ui = useObiStyles();
|
|
19
|
+
return (
|
|
20
|
+
<Card style={[ui.gapSm, style]}>
|
|
21
|
+
{rows.map(row => (
|
|
22
|
+
<View key={`${row.label}-${row.value}`} style={ui.formRow}>
|
|
23
|
+
<AppText muted={!row.strong} weight={row.strong ? '900' : undefined}>
|
|
24
|
+
{row.label}
|
|
25
|
+
</AppText>
|
|
26
|
+
<AppText weight="900">{row.value}</AppText>
|
|
27
|
+
</View>
|
|
28
|
+
))}
|
|
29
|
+
</Card>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import { AppText, useObiStyles } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export type SectionBlockProps = {
|
|
6
|
+
title: string;
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
style?: StyleProp<ViewStyle>;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Domain section chrome: uppercase label + content gap. */
|
|
12
|
+
export function SectionBlock({ title, children, style }: SectionBlockProps) {
|
|
13
|
+
const ui = useObiStyles();
|
|
14
|
+
return (
|
|
15
|
+
<View style={[ui.formSection, style]}>
|
|
16
|
+
<AppText style={ui.sectionLabel}>{title}</AppText>
|
|
17
|
+
{children}
|
|
18
|
+
</View>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { Card, ObiText, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export type DashboardActivityItem = {
|
|
6
|
+
key: string;
|
|
7
|
+
title: string;
|
|
8
|
+
subtitle: string;
|
|
9
|
+
amount: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function ActivityList({ items, emptyText = 'No hay actividad reciente para mostrar.' }: { items: DashboardActivityItem[]; emptyText?: string }) {
|
|
13
|
+
const theme = useObiTheme();
|
|
14
|
+
return (
|
|
15
|
+
<Card variant="glass" padding={0}>
|
|
16
|
+
{items.length ? items.map((item, index) => (
|
|
17
|
+
<View
|
|
18
|
+
key={item.key}
|
|
19
|
+
style={{
|
|
20
|
+
flexDirection: 'row',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
gap: theme.spacing.md,
|
|
23
|
+
padding: theme.spacing.md,
|
|
24
|
+
borderBottomWidth: index < items.length - 1 ? 1 : 0,
|
|
25
|
+
borderBottomColor: theme.colors.separator,
|
|
26
|
+
}}
|
|
27
|
+
>
|
|
28
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
29
|
+
<ObiText weight="800" numberOfLines={1}>{item.title}</ObiText>
|
|
30
|
+
<ObiText variant="caption" muted numberOfLines={1}>{item.subtitle}</ObiText>
|
|
31
|
+
</View>
|
|
32
|
+
<ObiText weight="800">{item.amount}</ObiText>
|
|
33
|
+
</View>
|
|
34
|
+
)) : (
|
|
35
|
+
<View style={{ padding: theme.spacing.md }}>
|
|
36
|
+
<ObiText muted>{emptyText}</ObiText>
|
|
37
|
+
</View>
|
|
38
|
+
)}
|
|
39
|
+
</Card>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { ObiText, shadow, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export type CompanyStatusItem = { label: string; value: string };
|
|
6
|
+
|
|
7
|
+
export function CompanyStatusGrid({ items }: { items: CompanyStatusItem[] }) {
|
|
8
|
+
const theme = useObiTheme();
|
|
9
|
+
return (
|
|
10
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: theme.spacing.md }}>
|
|
11
|
+
{items.map(item => (
|
|
12
|
+
<View
|
|
13
|
+
key={item.label}
|
|
14
|
+
style={{
|
|
15
|
+
flex: 1,
|
|
16
|
+
minWidth: 120,
|
|
17
|
+
backgroundColor: theme.glass.surface,
|
|
18
|
+
borderRadius: theme.radius.lg,
|
|
19
|
+
padding: theme.spacing.md,
|
|
20
|
+
gap: theme.spacing.xs,
|
|
21
|
+
borderWidth: 1,
|
|
22
|
+
borderColor: theme.glass.border,
|
|
23
|
+
...shadow.glassSm,
|
|
24
|
+
}}
|
|
25
|
+
>
|
|
26
|
+
<ObiText variant="caption" weight="700" muted style={{ letterSpacing: 0.8, textTransform: 'uppercase' }}>
|
|
27
|
+
{item.label}
|
|
28
|
+
</ObiText>
|
|
29
|
+
<ObiText weight="800">{item.value}</ObiText>
|
|
30
|
+
</View>
|
|
31
|
+
))}
|
|
32
|
+
</View>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { GlassSurface, IconButton, ObiText, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
5
|
+
|
|
6
|
+
export function DashboardHeader({
|
|
7
|
+
greeting,
|
|
8
|
+
company,
|
|
9
|
+
onMenuPress,
|
|
10
|
+
right,
|
|
11
|
+
}: {
|
|
12
|
+
greeting: string;
|
|
13
|
+
company: string;
|
|
14
|
+
onMenuPress: () => void;
|
|
15
|
+
right?: React.ReactNode;
|
|
16
|
+
}) {
|
|
17
|
+
const theme = useObiTheme();
|
|
18
|
+
const insets = useSafeAreaInsets();
|
|
19
|
+
return (
|
|
20
|
+
<GlassSurface
|
|
21
|
+
variant="chrome"
|
|
22
|
+
style={{
|
|
23
|
+
flexDirection: 'row',
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
gap: theme.spacing.md,
|
|
26
|
+
paddingHorizontal: theme.spacing.lg,
|
|
27
|
+
paddingTop: insets.top + theme.spacing.md,
|
|
28
|
+
paddingBottom: theme.spacing.md,
|
|
29
|
+
borderBottomWidth: 1,
|
|
30
|
+
borderBottomColor: theme.glass.borderSoft,
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<IconButton icon="menu" accessibilityLabel="Abrir menú" onPress={onMenuPress} variant="ghost" />
|
|
34
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
35
|
+
<ObiText variant="subtitle" weight="900" numberOfLines={1}>{greeting}</ObiText>
|
|
36
|
+
<ObiText variant="caption" muted numberOfLines={1}>{company}</ObiText>
|
|
37
|
+
</View>
|
|
38
|
+
{right}
|
|
39
|
+
</GlassSurface>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { ObiText, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export function DashboardSection({ title, children }: { title: string; children: React.ReactNode }) {
|
|
6
|
+
const theme = useObiTheme();
|
|
7
|
+
return (
|
|
8
|
+
<View style={{ gap: theme.spacing.md }}>
|
|
9
|
+
<ObiText variant="caption" weight="800" muted style={{ letterSpacing: 0.8, textTransform: 'uppercase' }}>
|
|
10
|
+
{title}
|
|
11
|
+
</ObiText>
|
|
12
|
+
{children}
|
|
13
|
+
</View>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
import { Icon, type IconName, ObiText, shadow, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export type DashboardQuickAction = {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon: IconName;
|
|
9
|
+
color: string;
|
|
10
|
+
onPress: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function QuickActionGrid({ actions }: { actions: DashboardQuickAction[] }) {
|
|
14
|
+
const theme = useObiTheme();
|
|
15
|
+
return (
|
|
16
|
+
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: theme.spacing.sm }}>
|
|
17
|
+
{actions.map(action => (
|
|
18
|
+
<Pressable
|
|
19
|
+
key={action.key}
|
|
20
|
+
accessibilityRole="button"
|
|
21
|
+
accessibilityLabel={action.label}
|
|
22
|
+
onPress={action.onPress}
|
|
23
|
+
style={({ pressed }) => ({
|
|
24
|
+
width: '31%',
|
|
25
|
+
minWidth: 96,
|
|
26
|
+
flexGrow: 1,
|
|
27
|
+
minHeight: 92,
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
justifyContent: 'center',
|
|
30
|
+
gap: theme.spacing.sm,
|
|
31
|
+
padding: theme.spacing.md,
|
|
32
|
+
borderRadius: theme.radius.lg,
|
|
33
|
+
backgroundColor: theme.glass.surface,
|
|
34
|
+
borderWidth: 1,
|
|
35
|
+
borderColor: theme.glass.border,
|
|
36
|
+
opacity: pressed ? 0.74 : 1,
|
|
37
|
+
...shadow.glassSm,
|
|
38
|
+
})}
|
|
39
|
+
>
|
|
40
|
+
<View
|
|
41
|
+
style={{
|
|
42
|
+
width: 42,
|
|
43
|
+
height: 42,
|
|
44
|
+
borderRadius: theme.radius.md,
|
|
45
|
+
alignItems: 'center',
|
|
46
|
+
justifyContent: 'center',
|
|
47
|
+
backgroundColor: `${action.color}18`,
|
|
48
|
+
}}
|
|
49
|
+
>
|
|
50
|
+
<Icon name={action.icon} size={22} color={action.color} />
|
|
51
|
+
</View>
|
|
52
|
+
<ObiText variant="caption" weight="800" style={{ textAlign: 'center' }} numberOfLines={2}>
|
|
53
|
+
{action.label}
|
|
54
|
+
</ObiText>
|
|
55
|
+
</Pressable>
|
|
56
|
+
))}
|
|
57
|
+
</View>
|
|
58
|
+
);
|
|
59
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { DashboardSection } from './dashboard/DashboardSection';
|
|
2
|
+
export { QuickActionGrid, type DashboardQuickAction } from './dashboard/QuickActionGrid';
|
|
3
|
+
export { ActivityList, type DashboardActivityItem } from './dashboard/ActivityList';
|
|
4
|
+
export { CompanyStatusGrid, type CompanyStatusItem } from './dashboard/CompanyStatusGrid';
|
|
5
|
+
export { DashboardHeader } from './dashboard/DashboardHeader';
|
|
6
|
+
|
|
7
|
+
export * from './pos';
|
|
8
|
+
export * from './checkout';
|
|
9
|
+
export * from './chrome';
|
|
10
|
+
export { useVentasStyles } from './styles/useVentasStyles';
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, ScrollView } from 'react-native';
|
|
3
|
+
import { AppText, Icon, type IconName, shadow, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export type PosCategoryChip = {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
icon?: IconName;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function PosCategoryChips({
|
|
12
|
+
items,
|
|
13
|
+
selectedKey,
|
|
14
|
+
onSelect,
|
|
15
|
+
}: {
|
|
16
|
+
items: PosCategoryChip[];
|
|
17
|
+
selectedKey: string;
|
|
18
|
+
onSelect: (key: string) => void;
|
|
19
|
+
}) {
|
|
20
|
+
const theme = useObiTheme();
|
|
21
|
+
return (
|
|
22
|
+
<ScrollView
|
|
23
|
+
horizontal
|
|
24
|
+
showsHorizontalScrollIndicator={false}
|
|
25
|
+
keyboardShouldPersistTaps="handled"
|
|
26
|
+
style={{ flexGrow: 0, flexShrink: 0 }}
|
|
27
|
+
contentContainerStyle={{ gap: 6, alignItems: 'center' }}
|
|
28
|
+
>
|
|
29
|
+
{items.map(item => {
|
|
30
|
+
const selected = item.key === selectedKey;
|
|
31
|
+
return (
|
|
32
|
+
<Pressable
|
|
33
|
+
key={item.key}
|
|
34
|
+
accessibilityRole="button"
|
|
35
|
+
accessibilityState={{ selected }}
|
|
36
|
+
onPress={() => onSelect(item.key)}
|
|
37
|
+
style={({ pressed }) => [
|
|
38
|
+
{
|
|
39
|
+
flexDirection: 'row',
|
|
40
|
+
alignItems: 'center',
|
|
41
|
+
gap: 5,
|
|
42
|
+
paddingHorizontal: 10,
|
|
43
|
+
paddingVertical: 5,
|
|
44
|
+
borderRadius: theme.radius.pill,
|
|
45
|
+
backgroundColor: selected ? theme.colors.primary : theme.glass.surface,
|
|
46
|
+
borderWidth: 1,
|
|
47
|
+
borderColor: selected ? theme.colors.primary : theme.glass.border,
|
|
48
|
+
maxWidth: 180,
|
|
49
|
+
opacity: pressed ? 0.74 : 1,
|
|
50
|
+
},
|
|
51
|
+
selected ? null : shadow.glassSm,
|
|
52
|
+
]}
|
|
53
|
+
>
|
|
54
|
+
{item.icon ? (
|
|
55
|
+
<Icon
|
|
56
|
+
name={item.icon}
|
|
57
|
+
size={14}
|
|
58
|
+
color={selected ? theme.colors.onPrimary : theme.colors.textMuted}
|
|
59
|
+
/>
|
|
60
|
+
) : null}
|
|
61
|
+
<AppText
|
|
62
|
+
numberOfLines={1}
|
|
63
|
+
weight="700"
|
|
64
|
+
style={{
|
|
65
|
+
fontSize: 12,
|
|
66
|
+
color: selected ? theme.colors.onPrimary : theme.colors.textMuted,
|
|
67
|
+
}}
|
|
68
|
+
>
|
|
69
|
+
{item.label}
|
|
70
|
+
</AppText>
|
|
71
|
+
</Pressable>
|
|
72
|
+
);
|
|
73
|
+
})}
|
|
74
|
+
</ScrollView>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable } from 'react-native';
|
|
3
|
+
import { AppText, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export function PosCategoryTile({
|
|
6
|
+
name,
|
|
7
|
+
backgroundColor,
|
|
8
|
+
foregroundColor,
|
|
9
|
+
selected,
|
|
10
|
+
onPress,
|
|
11
|
+
}: {
|
|
12
|
+
name: string;
|
|
13
|
+
backgroundColor: string;
|
|
14
|
+
foregroundColor: string;
|
|
15
|
+
selected?: boolean;
|
|
16
|
+
onPress: () => void;
|
|
17
|
+
}) {
|
|
18
|
+
const theme = useObiTheme();
|
|
19
|
+
return (
|
|
20
|
+
<Pressable
|
|
21
|
+
accessibilityRole="button"
|
|
22
|
+
accessibilityState={{ selected: Boolean(selected) }}
|
|
23
|
+
onPress={onPress}
|
|
24
|
+
style={({ pressed }) => ({
|
|
25
|
+
flex: 1,
|
|
26
|
+
minHeight: 72,
|
|
27
|
+
borderRadius: 8,
|
|
28
|
+
padding: theme.spacing.sm,
|
|
29
|
+
justifyContent: 'center',
|
|
30
|
+
overflow: 'hidden',
|
|
31
|
+
backgroundColor,
|
|
32
|
+
borderWidth: selected ? 2 : 0,
|
|
33
|
+
borderColor: selected ? theme.colors.primary : 'transparent',
|
|
34
|
+
opacity: pressed ? 0.74 : 1,
|
|
35
|
+
})}
|
|
36
|
+
>
|
|
37
|
+
<AppText numberOfLines={3} style={{ fontSize: 16, fontWeight: '700', textAlign: 'center', color: foregroundColor }}>
|
|
38
|
+
{name}
|
|
39
|
+
</AppText>
|
|
40
|
+
</Pressable>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { View } from 'react-native';
|
|
3
|
+
import { GlassSurface, shadow, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export function PosDock({ children }: { children: React.ReactNode }) {
|
|
6
|
+
const theme = useObiTheme();
|
|
7
|
+
return (
|
|
8
|
+
<GlassSurface
|
|
9
|
+
variant="dock"
|
|
10
|
+
style={{
|
|
11
|
+
flexDirection: 'row',
|
|
12
|
+
alignItems: 'stretch',
|
|
13
|
+
width: '100%',
|
|
14
|
+
gap: theme.spacing.sm,
|
|
15
|
+
paddingHorizontal: theme.spacing.sm,
|
|
16
|
+
paddingTop: theme.spacing.sm,
|
|
17
|
+
paddingBottom: theme.spacing.md,
|
|
18
|
+
backgroundColor: 'transparent',
|
|
19
|
+
zIndex: 20,
|
|
20
|
+
borderTopWidth: 1,
|
|
21
|
+
borderTopColor: theme.glass.borderSoft,
|
|
22
|
+
...shadow.top,
|
|
23
|
+
}}
|
|
24
|
+
>
|
|
25
|
+
<View style={{ flexDirection: 'row', width: '100%', gap: theme.spacing.sm }} collapsable={false}>
|
|
26
|
+
{children}
|
|
27
|
+
</View>
|
|
28
|
+
</GlassSurface>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Image, Pressable, View } from 'react-native';
|
|
3
|
+
import LinearGradient from 'react-native-linear-gradient';
|
|
4
|
+
import { AppText, useObiTheme } from '@obisoft/ui';
|
|
5
|
+
|
|
6
|
+
export type PosProductTileProps = {
|
|
7
|
+
name: string;
|
|
8
|
+
priceLabel: string;
|
|
9
|
+
stockLabel: string;
|
|
10
|
+
backgroundColor: string;
|
|
11
|
+
foregroundColor: string;
|
|
12
|
+
mutedForegroundColor: string;
|
|
13
|
+
imageUri?: string | null;
|
|
14
|
+
initial?: string;
|
|
15
|
+
available?: boolean;
|
|
16
|
+
selected?: boolean;
|
|
17
|
+
onPress: () => void;
|
|
18
|
+
onImageError?: () => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function PosProductTile({
|
|
22
|
+
name,
|
|
23
|
+
priceLabel,
|
|
24
|
+
stockLabel,
|
|
25
|
+
backgroundColor,
|
|
26
|
+
foregroundColor,
|
|
27
|
+
mutedForegroundColor,
|
|
28
|
+
imageUri,
|
|
29
|
+
initial = 'P',
|
|
30
|
+
available = true,
|
|
31
|
+
selected,
|
|
32
|
+
onPress,
|
|
33
|
+
onImageError,
|
|
34
|
+
}: PosProductTileProps) {
|
|
35
|
+
const theme = useObiTheme();
|
|
36
|
+
return (
|
|
37
|
+
<Pressable
|
|
38
|
+
accessibilityRole="button"
|
|
39
|
+
accessibilityState={{ disabled: !available, selected: Boolean(selected) }}
|
|
40
|
+
disabled={!available}
|
|
41
|
+
onPress={onPress}
|
|
42
|
+
style={({ pressed }) => ({
|
|
43
|
+
flex: 1,
|
|
44
|
+
minHeight: 72,
|
|
45
|
+
borderRadius: 8,
|
|
46
|
+
overflow: 'hidden',
|
|
47
|
+
justifyContent: 'flex-end',
|
|
48
|
+
backgroundColor,
|
|
49
|
+
borderWidth: selected ? 2 : 0,
|
|
50
|
+
borderColor: selected ? theme.colors.primary : 'transparent',
|
|
51
|
+
opacity: !available ? 0.42 : pressed ? 0.76 : 1,
|
|
52
|
+
})}
|
|
53
|
+
>
|
|
54
|
+
{imageUri ? (
|
|
55
|
+
<>
|
|
56
|
+
<Image
|
|
57
|
+
source={{ uri: imageUri }}
|
|
58
|
+
resizeMode="cover"
|
|
59
|
+
pointerEvents="none"
|
|
60
|
+
style={{ position: 'absolute', left: 0, right: 0, top: 0, bottom: 0 }}
|
|
61
|
+
onError={onImageError}
|
|
62
|
+
/>
|
|
63
|
+
<LinearGradient
|
|
64
|
+
pointerEvents="none"
|
|
65
|
+
colors={['rgba(15,23,42,0)', 'rgba(15,23,42,0.18)', 'rgba(15,23,42,0.72)', 'rgba(15,23,42,0.92)']}
|
|
66
|
+
locations={[0, 0.34, 0.72, 1]}
|
|
67
|
+
style={{ position: 'absolute', left: 0, right: 0, top: '24%', bottom: 0 }}
|
|
68
|
+
/>
|
|
69
|
+
</>
|
|
70
|
+
) : (
|
|
71
|
+
<View
|
|
72
|
+
pointerEvents="none"
|
|
73
|
+
style={{
|
|
74
|
+
position: 'absolute',
|
|
75
|
+
left: 0,
|
|
76
|
+
right: 0,
|
|
77
|
+
top: 0,
|
|
78
|
+
bottom: 0,
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
justifyContent: 'center',
|
|
81
|
+
backgroundColor,
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
<AppText style={{ fontSize: 22, fontWeight: '800', color: foregroundColor }}>{initial}</AppText>
|
|
85
|
+
</View>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
<View style={{ zIndex: 2, paddingHorizontal: 6, paddingTop: 4, paddingBottom: 5, gap: 2 }}>
|
|
89
|
+
<AppText
|
|
90
|
+
numberOfLines={1}
|
|
91
|
+
style={{
|
|
92
|
+
fontSize: 11,
|
|
93
|
+
fontWeight: '700',
|
|
94
|
+
lineHeight: 13,
|
|
95
|
+
color: foregroundColor,
|
|
96
|
+
textShadowColor: imageUri ? 'rgba(15,23,42,0.45)' : 'transparent',
|
|
97
|
+
textShadowOffset: { width: 0, height: 1 },
|
|
98
|
+
textShadowRadius: imageUri ? 2 : 0,
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{name}
|
|
102
|
+
</AppText>
|
|
103
|
+
<View style={{ flexDirection: 'row', alignItems: 'flex-end', justifyContent: 'space-between', gap: theme.spacing.sm }}>
|
|
104
|
+
<View style={{ flex: 1, minWidth: 0 }}>
|
|
105
|
+
<AppText style={{ fontSize: 8, fontWeight: '700', letterSpacing: 0.3, textTransform: 'uppercase', color: mutedForegroundColor }}>
|
|
106
|
+
Precio
|
|
107
|
+
</AppText>
|
|
108
|
+
<AppText numberOfLines={1} style={{ fontSize: 11, fontWeight: '800', lineHeight: 13, color: foregroundColor }}>
|
|
109
|
+
{priceLabel}
|
|
110
|
+
</AppText>
|
|
111
|
+
</View>
|
|
112
|
+
<View style={{ alignItems: 'flex-end', minWidth: 0 }}>
|
|
113
|
+
<AppText style={{ fontSize: 8, fontWeight: '700', letterSpacing: 0.3, textTransform: 'uppercase', color: mutedForegroundColor }}>
|
|
114
|
+
Existencia
|
|
115
|
+
</AppText>
|
|
116
|
+
<AppText numberOfLines={1} style={{ fontSize: 11, fontWeight: '800', lineHeight: 13, color: foregroundColor }}>
|
|
117
|
+
{stockLabel}
|
|
118
|
+
</AppText>
|
|
119
|
+
</View>
|
|
120
|
+
</View>
|
|
121
|
+
</View>
|
|
122
|
+
</Pressable>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
import { AppText, type IconName, Icon, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export type PosTabItem = {
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
selected?: boolean;
|
|
9
|
+
icon?: IconName;
|
|
10
|
+
onPress: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function PosTabBar({ items }: { items: PosTabItem[] }) {
|
|
14
|
+
const theme = useObiTheme();
|
|
15
|
+
return (
|
|
16
|
+
<View
|
|
17
|
+
style={{
|
|
18
|
+
flexDirection: 'row',
|
|
19
|
+
backgroundColor: theme.glass.chrome,
|
|
20
|
+
borderBottomWidth: 1,
|
|
21
|
+
borderBottomColor: theme.glass.borderSoft,
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
{items.map(item => (
|
|
25
|
+
<Pressable
|
|
26
|
+
key={item.key}
|
|
27
|
+
accessibilityRole="tab"
|
|
28
|
+
accessibilityState={{ selected: Boolean(item.selected) }}
|
|
29
|
+
onPress={item.onPress}
|
|
30
|
+
style={({ pressed }) => ({
|
|
31
|
+
flex: 1,
|
|
32
|
+
minHeight: 44,
|
|
33
|
+
paddingVertical: theme.spacing.sm,
|
|
34
|
+
paddingHorizontal: theme.spacing.xs,
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
flexDirection: 'row',
|
|
38
|
+
gap: 6,
|
|
39
|
+
borderBottomWidth: 2,
|
|
40
|
+
borderBottomColor: item.selected ? theme.colors.primary : 'transparent',
|
|
41
|
+
opacity: pressed ? 0.7 : 1,
|
|
42
|
+
})}
|
|
43
|
+
>
|
|
44
|
+
{item.icon ? (
|
|
45
|
+
<Icon
|
|
46
|
+
name={item.icon}
|
|
47
|
+
size={15}
|
|
48
|
+
color={item.selected ? theme.colors.primary : theme.colors.textMuted}
|
|
49
|
+
/>
|
|
50
|
+
) : null}
|
|
51
|
+
<AppText
|
|
52
|
+
numberOfLines={1}
|
|
53
|
+
style={{
|
|
54
|
+
fontSize: 12,
|
|
55
|
+
fontWeight: '800',
|
|
56
|
+
letterSpacing: 0.7,
|
|
57
|
+
color: item.selected ? theme.colors.primary : theme.colors.textMuted,
|
|
58
|
+
textTransform: 'uppercase',
|
|
59
|
+
}}
|
|
60
|
+
>
|
|
61
|
+
{item.label}
|
|
62
|
+
</AppText>
|
|
63
|
+
</Pressable>
|
|
64
|
+
))}
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { Pressable, ScrollView, Vibration, View } from 'react-native';
|
|
3
|
+
import { AppText, Icon, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export type PosTicketItem = {
|
|
6
|
+
id: string;
|
|
7
|
+
quantity: number;
|
|
8
|
+
title: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function PosTicket({
|
|
12
|
+
items,
|
|
13
|
+
totalLabel,
|
|
14
|
+
taxLabel,
|
|
15
|
+
emptyLabel = 'Toca un producto para agregarlo',
|
|
16
|
+
onEdit,
|
|
17
|
+
onRemove,
|
|
18
|
+
}: {
|
|
19
|
+
items: PosTicketItem[];
|
|
20
|
+
totalLabel: string;
|
|
21
|
+
taxLabel?: string;
|
|
22
|
+
emptyLabel?: string;
|
|
23
|
+
onEdit?: (id: string) => void;
|
|
24
|
+
onRemove?: (id: string) => void;
|
|
25
|
+
}) {
|
|
26
|
+
const theme = useObiTheme();
|
|
27
|
+
const [menuId, setMenuId] = useState<string | null>(null);
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (menuId && !items.some(item => item.id === menuId)) setMenuId(null);
|
|
31
|
+
}, [items, menuId]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!menuId) return;
|
|
35
|
+
const timer = setTimeout(() => setMenuId(null), 3500);
|
|
36
|
+
return () => clearTimeout(timer);
|
|
37
|
+
}, [menuId]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<View
|
|
41
|
+
style={{
|
|
42
|
+
flexDirection: 'row',
|
|
43
|
+
alignItems: 'flex-start',
|
|
44
|
+
backgroundColor: theme.glass.chrome,
|
|
45
|
+
paddingHorizontal: theme.spacing.lg,
|
|
46
|
+
paddingVertical: theme.spacing.md,
|
|
47
|
+
minHeight: 116,
|
|
48
|
+
maxHeight: 164,
|
|
49
|
+
gap: theme.spacing.md,
|
|
50
|
+
borderBottomWidth: 1,
|
|
51
|
+
borderBottomColor: theme.colors.separator,
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<ScrollView
|
|
55
|
+
style={{ flex: 1, maxHeight: 140 }}
|
|
56
|
+
contentContainerStyle={{ gap: theme.spacing.xs }}
|
|
57
|
+
showsVerticalScrollIndicator={false}
|
|
58
|
+
keyboardShouldPersistTaps="handled"
|
|
59
|
+
onScrollBeginDrag={() => setMenuId(null)}
|
|
60
|
+
>
|
|
61
|
+
{items.length ? items.map(item => {
|
|
62
|
+
const menuOpen = menuId === item.id;
|
|
63
|
+
return (
|
|
64
|
+
<View key={item.id} style={{ flexDirection: 'row', alignItems: 'center', gap: 6, minHeight: 28 }}>
|
|
65
|
+
<Pressable
|
|
66
|
+
accessibilityRole="button"
|
|
67
|
+
accessibilityLabel={`${item.quantity} × ${item.title}`}
|
|
68
|
+
accessibilityHint="Toca para editar. Mantén pulsado para eliminar."
|
|
69
|
+
delayLongPress={320}
|
|
70
|
+
onPress={() => {
|
|
71
|
+
if (menuOpen) {
|
|
72
|
+
setMenuId(null);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
onEdit?.(item.id);
|
|
76
|
+
}}
|
|
77
|
+
onLongPress={() => {
|
|
78
|
+
if (!onRemove) return;
|
|
79
|
+
Vibration.vibrate(12);
|
|
80
|
+
setMenuId(item.id);
|
|
81
|
+
}}
|
|
82
|
+
style={({ pressed }) => ({ flex: 1, minWidth: 0, paddingVertical: 3, opacity: pressed ? 0.7 : 1 })}
|
|
83
|
+
>
|
|
84
|
+
<AppText
|
|
85
|
+
numberOfLines={1}
|
|
86
|
+
style={{
|
|
87
|
+
fontSize: 16,
|
|
88
|
+
fontWeight: '800',
|
|
89
|
+
color: theme.colors.primary,
|
|
90
|
+
textDecorationLine: onEdit ? 'underline' : 'none',
|
|
91
|
+
textDecorationColor: theme.colors.primary,
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{item.quantity} × {item.title}
|
|
95
|
+
</AppText>
|
|
96
|
+
</Pressable>
|
|
97
|
+
<View style={{ width: 36, height: 36, alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
|
|
98
|
+
{menuOpen && onRemove ? (
|
|
99
|
+
<Pressable
|
|
100
|
+
accessibilityRole="button"
|
|
101
|
+
accessibilityLabel={`Eliminar ${item.title}`}
|
|
102
|
+
hitSlop={6}
|
|
103
|
+
onPress={() => {
|
|
104
|
+
onRemove(item.id);
|
|
105
|
+
setMenuId(null);
|
|
106
|
+
}}
|
|
107
|
+
style={({ pressed }) => ({
|
|
108
|
+
width: 36,
|
|
109
|
+
height: 36,
|
|
110
|
+
borderRadius: theme.radius.sm,
|
|
111
|
+
alignItems: 'center',
|
|
112
|
+
justifyContent: 'center',
|
|
113
|
+
backgroundColor: theme.colors.dangerSoft,
|
|
114
|
+
borderWidth: 1,
|
|
115
|
+
borderColor: theme.colors.dangerBorder,
|
|
116
|
+
opacity: pressed ? 0.72 : 1,
|
|
117
|
+
})}
|
|
118
|
+
>
|
|
119
|
+
<Icon name="trash-2" size={16} color={theme.colors.danger} />
|
|
120
|
+
</Pressable>
|
|
121
|
+
) : onEdit ? (
|
|
122
|
+
<Icon name="pencil" size={14} color={theme.colors.textMuted} />
|
|
123
|
+
) : null}
|
|
124
|
+
</View>
|
|
125
|
+
</View>
|
|
126
|
+
);
|
|
127
|
+
}) : (
|
|
128
|
+
<AppText muted style={{ paddingTop: theme.spacing.xs }}>{emptyLabel}</AppText>
|
|
129
|
+
)}
|
|
130
|
+
</ScrollView>
|
|
131
|
+
<View style={{ minWidth: 118, alignItems: 'flex-end', justifyContent: 'flex-start' }}>
|
|
132
|
+
<AppText
|
|
133
|
+
numberOfLines={1}
|
|
134
|
+
adjustsFontSizeToFit
|
|
135
|
+
minimumFontScale={0.7}
|
|
136
|
+
style={{
|
|
137
|
+
fontSize: 46,
|
|
138
|
+
lineHeight: 50,
|
|
139
|
+
fontWeight: '800',
|
|
140
|
+
letterSpacing: -1.4,
|
|
141
|
+
color: theme.colors.text,
|
|
142
|
+
}}
|
|
143
|
+
>
|
|
144
|
+
{totalLabel}
|
|
145
|
+
</AppText>
|
|
146
|
+
{taxLabel ? <AppText muted style={{ marginTop: 2, fontSize: 11, fontWeight: '700' }}>{taxLabel}</AppText> : null}
|
|
147
|
+
</View>
|
|
148
|
+
</View>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Pressable, View } from 'react-native';
|
|
3
|
+
import { AppText, Icon, shadow, useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export function QuantityStepper({
|
|
6
|
+
value,
|
|
7
|
+
onDecrement,
|
|
8
|
+
onIncrement,
|
|
9
|
+
disabled,
|
|
10
|
+
decrementDisabled,
|
|
11
|
+
incrementDisabled,
|
|
12
|
+
}: {
|
|
13
|
+
value: string | number;
|
|
14
|
+
onDecrement: () => void;
|
|
15
|
+
onIncrement: () => void;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
decrementDisabled?: boolean;
|
|
18
|
+
incrementDisabled?: boolean;
|
|
19
|
+
}) {
|
|
20
|
+
const theme = useObiTheme();
|
|
21
|
+
const makeButtonStyle = (buttonDisabled: boolean) => ({ pressed }: { pressed: boolean }) => ({
|
|
22
|
+
width: 36,
|
|
23
|
+
height: 36,
|
|
24
|
+
borderRadius: theme.radius.sm,
|
|
25
|
+
alignItems: 'center' as const,
|
|
26
|
+
justifyContent: 'center' as const,
|
|
27
|
+
backgroundColor: theme.colors.surfaceMuted,
|
|
28
|
+
opacity: buttonDisabled ? 0.35 : pressed ? 0.72 : 1,
|
|
29
|
+
});
|
|
30
|
+
const decDisabled = Boolean(disabled || decrementDisabled);
|
|
31
|
+
const incDisabled = Boolean(disabled || incrementDisabled);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<View
|
|
35
|
+
style={{
|
|
36
|
+
flexDirection: 'row',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
gap: theme.spacing.sm,
|
|
39
|
+
minHeight: 48,
|
|
40
|
+
borderWidth: 1,
|
|
41
|
+
borderColor: theme.colors.border,
|
|
42
|
+
borderRadius: theme.radius.md,
|
|
43
|
+
backgroundColor: theme.glass.input,
|
|
44
|
+
paddingHorizontal: theme.spacing.sm,
|
|
45
|
+
opacity: disabled ? 0.55 : 1,
|
|
46
|
+
...shadow.xs,
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
<Pressable disabled={decDisabled} onPress={onDecrement} style={makeButtonStyle(decDisabled)} accessibilityLabel="Disminuir cantidad">
|
|
50
|
+
<Icon name="minus" size={16} color={theme.colors.text} />
|
|
51
|
+
</Pressable>
|
|
52
|
+
<AppText style={{ flex: 1, textAlign: 'center', fontSize: 18, fontWeight: '800', color: theme.colors.text }}>
|
|
53
|
+
{value}
|
|
54
|
+
</AppText>
|
|
55
|
+
<Pressable disabled={incDisabled} onPress={onIncrement} style={makeButtonStyle(incDisabled)} accessibilityLabel="Aumentar cantidad">
|
|
56
|
+
<Icon name="plus" size={16} color={theme.colors.text} />
|
|
57
|
+
</Pressable>
|
|
58
|
+
</View>
|
|
59
|
+
);
|
|
60
|
+
}
|
package/src/pos/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { PosTabBar, type PosTabItem } from './PosTabBar';
|
|
2
|
+
export { PosCategoryChips, type PosCategoryChip } from './PosCategoryChips';
|
|
3
|
+
export { PosProductTile, type PosProductTileProps } from './PosProductTile';
|
|
4
|
+
export { PosCategoryTile } from './PosCategoryTile';
|
|
5
|
+
export { PosDock } from './PosDock';
|
|
6
|
+
export { PosTicket, type PosTicketItem } from './PosTicket';
|
|
7
|
+
export { QuantityStepper } from './QuantityStepper';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { StyleSheet } from 'react-native';
|
|
3
|
+
import { useObiTheme } from '@obisoft/ui';
|
|
4
|
+
|
|
5
|
+
export function useVentasStyles() {
|
|
6
|
+
const theme = useObiTheme();
|
|
7
|
+
return useMemo(() => StyleSheet.create({
|
|
8
|
+
cartEditorLabel: {
|
|
9
|
+
fontSize: 11,
|
|
10
|
+
fontWeight: '700',
|
|
11
|
+
color: theme.colors.textMuted,
|
|
12
|
+
letterSpacing: 0.4,
|
|
13
|
+
textTransform: 'uppercase',
|
|
14
|
+
},
|
|
15
|
+
cartEditorTotalRow: {
|
|
16
|
+
flexDirection: 'row',
|
|
17
|
+
alignItems: 'baseline',
|
|
18
|
+
justifyContent: 'space-between',
|
|
19
|
+
paddingVertical: theme.spacing.sm,
|
|
20
|
+
},
|
|
21
|
+
cartEditorTotalValue: {
|
|
22
|
+
fontSize: 22,
|
|
23
|
+
fontWeight: '800',
|
|
24
|
+
color: theme.colors.text,
|
|
25
|
+
},
|
|
26
|
+
posRoot: { flex: 1, backgroundColor: 'transparent' },
|
|
27
|
+
posCatalogHeader: {
|
|
28
|
+
paddingHorizontal: theme.spacing.sm,
|
|
29
|
+
paddingTop: theme.spacing.sm,
|
|
30
|
+
paddingBottom: 4,
|
|
31
|
+
gap: 6,
|
|
32
|
+
},
|
|
33
|
+
}), [theme]);
|
|
34
|
+
}
|