@apptimate/ui 1.0.0 → 1.2.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/CHANGELOG.md +7 -7
- package/package.json +22 -18
- package/src/{Button.tsx → base-components/Button.tsx} +21 -13
- package/src/{ChipInput.tsx → base-components/ChipInput.tsx} +1 -1
- package/src/base-components/DateLabel.tsx +68 -0
- package/src/base-components/DateRangePicker.tsx +392 -0
- package/src/base-components/DesktopFilterPopover.tsx +53 -0
- package/src/base-components/DesktopSearchPopover.tsx +51 -0
- package/src/{Dropdown.tsx → base-components/Dropdown.tsx} +11 -5
- package/src/base-components/ImageDropzone.tsx +80 -0
- package/src/{Input.tsx → base-components/Input.tsx} +8 -7
- package/src/base-components/Label.tsx +24 -0
- package/src/{Modal.tsx → base-components/Modal.tsx} +36 -10
- package/src/{PopConfirm.tsx → base-components/PopConfirm.tsx} +15 -4
- package/src/base-components/RecordCount.tsx +19 -0
- package/src/{SearchableSelect.tsx → base-components/SearchableSelect.tsx} +9 -9
- package/src/{Select.tsx → base-components/Select.tsx} +7 -6
- package/src/base-components/Table.tsx +83 -0
- package/src/base-components/TableMobileOptions.tsx +51 -0
- package/src/common-components/DashboardLayout.tsx +473 -0
- package/src/common-components/charts/ApexCharts.tsx +222 -0
- package/src/common-components/charts/RBarChart.tsx +145 -0
- package/src/common-components/charts/RLineChart.tsx +116 -0
- package/src/common-components/charts/index.ts +3 -0
- package/src/common-components/charts/palette.ts +10 -0
- package/src/components/shared/CustomerSelectorComponent.tsx +163 -0
- package/src/components/shared/ImageUploadComponent.tsx +228 -0
- package/src/components/shared/PaymentModeComponent.tsx +421 -0
- package/src/components/shared/ProcurementManagerSelector.tsx +5 -0
- package/src/components/shared/ProductSelectorComponent.tsx +166 -0
- package/src/components/shared/SupplierSelectorComponent.tsx +5 -0
- package/src/index.tsx +30 -18
- package/src/Table.tsx +0 -78
- /package/src/{Accordion.tsx → base-components/Accordion.tsx} +0 -0
- /package/src/{Badge.tsx → base-components/Badge.tsx} +0 -0
- /package/src/{ButtonGroup.tsx → base-components/ButtonGroup.tsx} +0 -0
- /package/src/{Card.tsx → base-components/Card.tsx} +0 -0
- /package/src/{Checkbox.tsx → base-components/Checkbox.tsx} +0 -0
- /package/src/{Divider.tsx → base-components/Divider.tsx} +0 -0
- /package/src/{EmptyState.tsx → base-components/EmptyState.tsx} +0 -0
- /package/src/{HintIcon.tsx → base-components/HintIcon.tsx} +0 -0
- /package/src/{Pagination.tsx → base-components/Pagination.tsx} +0 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React, { useMemo } from "react";
|
|
4
|
+
import dynamic from "next/dynamic";
|
|
5
|
+
|
|
6
|
+
const ApexChart = dynamic(() => import("react-apexcharts"), { ssr: false });
|
|
7
|
+
|
|
8
|
+
// ── Shared defaults ──────────────────────────────────────────────────
|
|
9
|
+
const defaultFont = "inherit";
|
|
10
|
+
const defaultAxisStyle = { fontSize: "11px", fontWeight: 600, colors: "#94a3b8" } as const;
|
|
11
|
+
const defaultGrid: ApexCharts.ApexOptions["grid"] = {
|
|
12
|
+
borderColor: "#f1f5f9", strokeDashArray: 0,
|
|
13
|
+
xaxis: { lines: { show: false } }, yaxis: { lines: { show: true } },
|
|
14
|
+
padding: { left: 8, right: 8 },
|
|
15
|
+
};
|
|
16
|
+
const defaultAnimations: ApexCharts.ApexOptions["chart"] = {
|
|
17
|
+
animations: { enabled: true, speed: 1000, animateGradually: { enabled: true, delay: 150 }, dynamicAnimation: { enabled: true, speed: 400 } },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
21
|
+
export interface ChartSeries { name: string; data: number[]; color?: string; }
|
|
22
|
+
|
|
23
|
+
export interface AreaChartProps {
|
|
24
|
+
series: ChartSeries[];
|
|
25
|
+
categories: string[];
|
|
26
|
+
height?: number;
|
|
27
|
+
colors?: string[];
|
|
28
|
+
yFormatter?: (val: number) => string;
|
|
29
|
+
xFormatter?: (val: string) => string;
|
|
30
|
+
tooltipYFormatter?: (val: number) => string;
|
|
31
|
+
tooltipXFormatter?: (val: any, opts?: any) => string;
|
|
32
|
+
showToolbar?: boolean;
|
|
33
|
+
showLegend?: boolean;
|
|
34
|
+
legendPosition?: "top" | "bottom";
|
|
35
|
+
enableZoom?: boolean;
|
|
36
|
+
strokeWidth?: number;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LineChartProps extends AreaChartProps {
|
|
41
|
+
strokeDashArray?: number[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BarChartProps {
|
|
45
|
+
series: ChartSeries[];
|
|
46
|
+
categories: string[];
|
|
47
|
+
height?: number;
|
|
48
|
+
colors?: string[];
|
|
49
|
+
horizontal?: boolean;
|
|
50
|
+
stacked?: boolean;
|
|
51
|
+
yFormatter?: (val: number) => string;
|
|
52
|
+
tooltipYFormatter?: (val: number) => string;
|
|
53
|
+
showToolbar?: boolean;
|
|
54
|
+
showLegend?: boolean;
|
|
55
|
+
borderRadius?: number;
|
|
56
|
+
columnWidth?: string;
|
|
57
|
+
className?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface DonutChartProps {
|
|
61
|
+
series: number[];
|
|
62
|
+
labels: string[];
|
|
63
|
+
height?: number;
|
|
64
|
+
colors?: string[];
|
|
65
|
+
showLegend?: boolean;
|
|
66
|
+
legendPosition?: "top" | "bottom" | "right" | "left";
|
|
67
|
+
className?: string;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Area Chart ───────────────────────────────────────────────────────
|
|
71
|
+
export function AreaChart({
|
|
72
|
+
series, categories, height = 350, colors,
|
|
73
|
+
yFormatter = (v) => String(v),
|
|
74
|
+
xFormatter = (v) => v,
|
|
75
|
+
tooltipYFormatter,
|
|
76
|
+
tooltipXFormatter,
|
|
77
|
+
showToolbar = true, showLegend = true, legendPosition = "top",
|
|
78
|
+
enableZoom = true, strokeWidth = 2.5, className = "",
|
|
79
|
+
}: AreaChartProps) {
|
|
80
|
+
const chartColors = colors || series.map(s => s.color).filter(Boolean) as string[];
|
|
81
|
+
|
|
82
|
+
const options = useMemo<ApexCharts.ApexOptions>(() => ({
|
|
83
|
+
chart: {
|
|
84
|
+
type: "area", height, fontFamily: defaultFont, ...defaultAnimations,
|
|
85
|
+
toolbar: { show: showToolbar },
|
|
86
|
+
zoom: { enabled: enableZoom, type: "x", autoScaleYaxis: true },
|
|
87
|
+
},
|
|
88
|
+
colors: chartColors.length ? chartColors : undefined,
|
|
89
|
+
stroke: { curve: "smooth", width: strokeWidth },
|
|
90
|
+
fill: { type: "gradient", gradient: { shadeIntensity: 1, opacityFrom: 0.25, opacityTo: 0.02, stops: [0, 95, 100] } },
|
|
91
|
+
dataLabels: { enabled: false },
|
|
92
|
+
xaxis: {
|
|
93
|
+
categories, type: "category",
|
|
94
|
+
labels: { style: defaultAxisStyle, rotate: -45, rotateAlways: false, formatter: xFormatter },
|
|
95
|
+
axisBorder: { show: false }, axisTicks: { show: false },
|
|
96
|
+
tickAmount: Math.min(categories.length, 12),
|
|
97
|
+
},
|
|
98
|
+
yaxis: { labels: { style: defaultAxisStyle, formatter: yFormatter } },
|
|
99
|
+
grid: defaultGrid,
|
|
100
|
+
tooltip: {
|
|
101
|
+
theme: "dark", shared: true, intersect: false,
|
|
102
|
+
x: tooltipXFormatter ? { formatter: tooltipXFormatter } : undefined,
|
|
103
|
+
y: { formatter: tooltipYFormatter || yFormatter },
|
|
104
|
+
style: { fontSize: "12px" },
|
|
105
|
+
},
|
|
106
|
+
legend: { show: showLegend, position: legendPosition, horizontalAlign: "center", fontSize: "12px", fontWeight: 700, markers: { size: 5, shape: "circle" as const }, itemMargin: { horizontal: 12, vertical: 4 } },
|
|
107
|
+
markers: { size: 0, hover: { size: 6, sizeOffset: 3 } },
|
|
108
|
+
}), [categories, chartColors, height, showToolbar, enableZoom, strokeWidth, showLegend, legendPosition]);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<div className={`w-full ${className}`}>
|
|
112
|
+
<ApexChart options={options} series={series} type="area" height={height} width="100%" />
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Line Chart ───────────────────────────────────────────────────────
|
|
118
|
+
export function LineChart({
|
|
119
|
+
series, categories, height = 350, colors,
|
|
120
|
+
yFormatter = (v) => String(v),
|
|
121
|
+
xFormatter = (v) => v,
|
|
122
|
+
tooltipYFormatter,
|
|
123
|
+
showToolbar = true, showLegend = true, legendPosition = "top",
|
|
124
|
+
enableZoom = true, strokeWidth = 2.5, strokeDashArray, className = "",
|
|
125
|
+
}: LineChartProps) {
|
|
126
|
+
const chartColors = colors || series.map(s => s.color).filter(Boolean) as string[];
|
|
127
|
+
|
|
128
|
+
const options = useMemo<ApexCharts.ApexOptions>(() => ({
|
|
129
|
+
chart: {
|
|
130
|
+
type: "line", height, fontFamily: defaultFont, ...defaultAnimations,
|
|
131
|
+
toolbar: { show: showToolbar },
|
|
132
|
+
zoom: { enabled: enableZoom, type: "x", autoScaleYaxis: true },
|
|
133
|
+
},
|
|
134
|
+
colors: chartColors.length ? chartColors : undefined,
|
|
135
|
+
stroke: { curve: "smooth", width: strokeWidth, dashArray: strokeDashArray },
|
|
136
|
+
dataLabels: { enabled: false },
|
|
137
|
+
xaxis: {
|
|
138
|
+
categories, type: "category",
|
|
139
|
+
labels: { style: defaultAxisStyle, rotate: -45, rotateAlways: false, formatter: xFormatter },
|
|
140
|
+
axisBorder: { show: false }, axisTicks: { show: false },
|
|
141
|
+
tickAmount: Math.min(categories.length, 12),
|
|
142
|
+
},
|
|
143
|
+
yaxis: { labels: { style: defaultAxisStyle, formatter: yFormatter } },
|
|
144
|
+
grid: defaultGrid,
|
|
145
|
+
tooltip: {
|
|
146
|
+
theme: "dark", shared: true, intersect: false,
|
|
147
|
+
y: { formatter: tooltipYFormatter || yFormatter },
|
|
148
|
+
style: { fontSize: "12px" },
|
|
149
|
+
},
|
|
150
|
+
legend: { show: showLegend, position: legendPosition, horizontalAlign: "center", fontSize: "12px", fontWeight: 700, markers: { size: 5, shape: "circle" as const }, itemMargin: { horizontal: 12, vertical: 4 } },
|
|
151
|
+
markers: { size: 4, hover: { size: 6, sizeOffset: 3 } },
|
|
152
|
+
}), [categories, chartColors, height, showToolbar, enableZoom, strokeWidth, strokeDashArray, showLegend, legendPosition]);
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
<div className={`w-full ${className}`}>
|
|
156
|
+
<ApexChart options={options} series={series} type="line" height={height} width="100%" />
|
|
157
|
+
</div>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ── Bar Chart ────────────────────────────────────────────────────────
|
|
162
|
+
export function BarChart({
|
|
163
|
+
series, categories, height = 350, colors,
|
|
164
|
+
horizontal = false, stacked = false,
|
|
165
|
+
yFormatter = (v) => String(v),
|
|
166
|
+
tooltipYFormatter,
|
|
167
|
+
showToolbar = false, showLegend = true,
|
|
168
|
+
borderRadius = 6, columnWidth = "50%", className = "",
|
|
169
|
+
}: BarChartProps) {
|
|
170
|
+
const chartColors = colors || series.map(s => s.color).filter(Boolean) as string[];
|
|
171
|
+
|
|
172
|
+
const options = useMemo<ApexCharts.ApexOptions>(() => ({
|
|
173
|
+
chart: {
|
|
174
|
+
type: "bar", height, fontFamily: defaultFont, stacked, ...defaultAnimations,
|
|
175
|
+
toolbar: { show: showToolbar },
|
|
176
|
+
},
|
|
177
|
+
colors: chartColors.length ? chartColors : undefined,
|
|
178
|
+
plotOptions: { bar: { horizontal, borderRadius, columnWidth, borderRadiusApplication: "end" as const } },
|
|
179
|
+
dataLabels: { enabled: false },
|
|
180
|
+
xaxis: {
|
|
181
|
+
categories,
|
|
182
|
+
labels: { style: defaultAxisStyle },
|
|
183
|
+
axisBorder: { show: false }, axisTicks: { show: false },
|
|
184
|
+
},
|
|
185
|
+
yaxis: { labels: { style: defaultAxisStyle, formatter: yFormatter } },
|
|
186
|
+
grid: defaultGrid,
|
|
187
|
+
tooltip: {
|
|
188
|
+
theme: "dark", shared: true, intersect: false,
|
|
189
|
+
y: { formatter: tooltipYFormatter || yFormatter },
|
|
190
|
+
style: { fontSize: "12px" },
|
|
191
|
+
},
|
|
192
|
+
legend: { show: showLegend, position: "top", horizontalAlign: "center", fontSize: "12px", fontWeight: 700, markers: { size: 5, shape: "circle" as const }, itemMargin: { horizontal: 12, vertical: 4 } },
|
|
193
|
+
}), [categories, chartColors, height, horizontal, stacked, showToolbar, showLegend, borderRadius, columnWidth]);
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div className={`w-full ${className}`}>
|
|
197
|
+
<ApexChart options={options} series={series} type="bar" height={height} width="100%" />
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ── Donut Chart ──────────────────────────────────────────────────────
|
|
203
|
+
export function DonutChart({
|
|
204
|
+
series, labels, height = 300, colors,
|
|
205
|
+
showLegend = true, legendPosition = "bottom", className = "",
|
|
206
|
+
}: DonutChartProps) {
|
|
207
|
+
const options = useMemo<ApexCharts.ApexOptions>(() => ({
|
|
208
|
+
chart: { type: "donut", height, fontFamily: defaultFont, ...defaultAnimations },
|
|
209
|
+
colors,
|
|
210
|
+
labels,
|
|
211
|
+
dataLabels: { enabled: true, style: { fontSize: "12px", fontWeight: 700 } },
|
|
212
|
+
plotOptions: { pie: { donut: { size: "65%", labels: { show: true, name: { fontSize: "14px", fontWeight: 700 }, value: { fontSize: "20px", fontWeight: 800 }, total: { show: true, fontSize: "13px", fontWeight: 600, label: "Total" } } } } },
|
|
213
|
+
legend: { show: showLegend, position: legendPosition, fontSize: "12px", fontWeight: 600, markers: { size: 5, shape: "circle" as const }, itemMargin: { horizontal: 8, vertical: 4 } },
|
|
214
|
+
stroke: { width: 2, colors: ["#fff"] },
|
|
215
|
+
}), [labels, colors, height, showLegend, legendPosition]);
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<div className={`w-full ${className}`}>
|
|
219
|
+
<ApexChart options={options} series={series} type="donut" height={height} width="100%" />
|
|
220
|
+
</div>
|
|
221
|
+
);
|
|
222
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import {
|
|
5
|
+
BarChart,
|
|
6
|
+
Bar,
|
|
7
|
+
XAxis,
|
|
8
|
+
YAxis,
|
|
9
|
+
CartesianGrid,
|
|
10
|
+
Tooltip,
|
|
11
|
+
Legend,
|
|
12
|
+
ResponsiveContainer,
|
|
13
|
+
} from "recharts";
|
|
14
|
+
import { CHART_COLORS } from "./palette";
|
|
15
|
+
|
|
16
|
+
export interface RBarChartSeries {
|
|
17
|
+
dataKey: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
color?: string;
|
|
20
|
+
stackId?: string;
|
|
21
|
+
radius?: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface RBarChartProps {
|
|
25
|
+
data: Record<string, any>[];
|
|
26
|
+
series: RBarChartSeries[];
|
|
27
|
+
xAxisKey: string;
|
|
28
|
+
height?: number;
|
|
29
|
+
colors?: string[];
|
|
30
|
+
layout?: "horizontal" | "vertical";
|
|
31
|
+
barSize?: number;
|
|
32
|
+
showGrid?: boolean;
|
|
33
|
+
showTooltip?: boolean;
|
|
34
|
+
showLegend?: boolean;
|
|
35
|
+
legendPosition?: "top" | "bottom";
|
|
36
|
+
xAxisFormatter?: (value: any) => string;
|
|
37
|
+
yAxisFormatter?: (value: any) => string;
|
|
38
|
+
tooltipFormatter?: (value: any, name: string) => [string, string];
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function RBarChart({
|
|
43
|
+
data,
|
|
44
|
+
series,
|
|
45
|
+
xAxisKey,
|
|
46
|
+
height = 350,
|
|
47
|
+
colors = [...CHART_COLORS],
|
|
48
|
+
layout = "horizontal",
|
|
49
|
+
barSize,
|
|
50
|
+
showGrid = true,
|
|
51
|
+
showTooltip = true,
|
|
52
|
+
showLegend = true,
|
|
53
|
+
legendPosition = "bottom",
|
|
54
|
+
xAxisFormatter,
|
|
55
|
+
yAxisFormatter,
|
|
56
|
+
tooltipFormatter,
|
|
57
|
+
className = "",
|
|
58
|
+
}: RBarChartProps) {
|
|
59
|
+
return (
|
|
60
|
+
<div className={`w-full ${className}`} style={{ height }}>
|
|
61
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
62
|
+
<BarChart data={data} layout={layout} margin={{ top: 10, right: 10, left: 0, bottom: 10 }}>
|
|
63
|
+
{showGrid && (
|
|
64
|
+
<CartesianGrid stroke="#f1f5f9" strokeDasharray="0" />
|
|
65
|
+
)}
|
|
66
|
+
{layout === "horizontal" ? (
|
|
67
|
+
<>
|
|
68
|
+
<XAxis
|
|
69
|
+
dataKey={xAxisKey}
|
|
70
|
+
tickFormatter={xAxisFormatter}
|
|
71
|
+
tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
|
|
72
|
+
tickLine={false}
|
|
73
|
+
axisLine={false}
|
|
74
|
+
/>
|
|
75
|
+
<YAxis
|
|
76
|
+
tickFormatter={yAxisFormatter}
|
|
77
|
+
tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
|
|
78
|
+
tickLine={false}
|
|
79
|
+
axisLine={false}
|
|
80
|
+
/>
|
|
81
|
+
</>
|
|
82
|
+
) : (
|
|
83
|
+
<>
|
|
84
|
+
<XAxis
|
|
85
|
+
type="number"
|
|
86
|
+
tickFormatter={xAxisFormatter}
|
|
87
|
+
tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
|
|
88
|
+
tickLine={false}
|
|
89
|
+
axisLine={false}
|
|
90
|
+
/>
|
|
91
|
+
<YAxis
|
|
92
|
+
type="category"
|
|
93
|
+
dataKey={xAxisKey}
|
|
94
|
+
tickFormatter={yAxisFormatter}
|
|
95
|
+
tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
|
|
96
|
+
tickLine={false}
|
|
97
|
+
axisLine={false}
|
|
98
|
+
/>
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
{showTooltip && (
|
|
102
|
+
<Tooltip
|
|
103
|
+
formatter={tooltipFormatter}
|
|
104
|
+
contentStyle={{
|
|
105
|
+
backgroundColor: "#1e293b",
|
|
106
|
+
color: "#fff",
|
|
107
|
+
borderRadius: "8px",
|
|
108
|
+
border: "none",
|
|
109
|
+
}}
|
|
110
|
+
itemStyle={{ color: "#fff" }}
|
|
111
|
+
cursor={{ fill: "transparent" }}
|
|
112
|
+
animationDuration={1000}
|
|
113
|
+
/>
|
|
114
|
+
)}
|
|
115
|
+
{showLegend && (
|
|
116
|
+
<Legend
|
|
117
|
+
verticalAlign={legendPosition}
|
|
118
|
+
wrapperStyle={{ fontSize: "12px", fontWeight: 700 }}
|
|
119
|
+
iconType="circle"
|
|
120
|
+
/>
|
|
121
|
+
)}
|
|
122
|
+
{series.map((s, idx) => {
|
|
123
|
+
const radius = s.radius !== undefined ? s.radius : 4;
|
|
124
|
+
const radiusArray =
|
|
125
|
+
layout === "vertical" ? [0, radius, radius, 0] : [radius, radius, 0, 0];
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<Bar
|
|
129
|
+
key={s.dataKey}
|
|
130
|
+
dataKey={s.dataKey}
|
|
131
|
+
name={s.name || s.dataKey}
|
|
132
|
+
fill={s.color || colors[idx % colors.length]}
|
|
133
|
+
stackId={s.stackId}
|
|
134
|
+
barSize={barSize}
|
|
135
|
+
radius={radiusArray as any}
|
|
136
|
+
isAnimationActive={true}
|
|
137
|
+
animationDuration={1000}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
})}
|
|
141
|
+
</BarChart>
|
|
142
|
+
</ResponsiveContainer>
|
|
143
|
+
</div>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import {
|
|
5
|
+
LineChart,
|
|
6
|
+
Line,
|
|
7
|
+
XAxis,
|
|
8
|
+
YAxis,
|
|
9
|
+
CartesianGrid,
|
|
10
|
+
Tooltip,
|
|
11
|
+
Legend,
|
|
12
|
+
ResponsiveContainer,
|
|
13
|
+
} from "recharts";
|
|
14
|
+
import { CHART_COLORS } from "./palette";
|
|
15
|
+
|
|
16
|
+
export interface RLineChartSeries {
|
|
17
|
+
dataKey: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
color?: string;
|
|
20
|
+
strokeWidth?: number;
|
|
21
|
+
type?: "monotone" | "linear" | "step" | "basis";
|
|
22
|
+
dashed?: boolean;
|
|
23
|
+
showDot?: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RLineChartProps {
|
|
27
|
+
data: Record<string, any>[];
|
|
28
|
+
series: RLineChartSeries[];
|
|
29
|
+
xAxisKey: string;
|
|
30
|
+
height?: number;
|
|
31
|
+
colors?: string[];
|
|
32
|
+
showGrid?: boolean;
|
|
33
|
+
showTooltip?: boolean;
|
|
34
|
+
showLegend?: boolean;
|
|
35
|
+
legendPosition?: "top" | "bottom";
|
|
36
|
+
xAxisFormatter?: (value: any) => string;
|
|
37
|
+
yAxisFormatter?: (value: any) => string;
|
|
38
|
+
tooltipFormatter?: (value: any, name: string) => [string, string];
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function RLineChart({
|
|
43
|
+
data,
|
|
44
|
+
series,
|
|
45
|
+
xAxisKey,
|
|
46
|
+
height = 350,
|
|
47
|
+
colors = [...CHART_COLORS],
|
|
48
|
+
showGrid = true,
|
|
49
|
+
showTooltip = true,
|
|
50
|
+
showLegend = true,
|
|
51
|
+
legendPosition = "bottom",
|
|
52
|
+
xAxisFormatter,
|
|
53
|
+
yAxisFormatter,
|
|
54
|
+
tooltipFormatter,
|
|
55
|
+
className = "",
|
|
56
|
+
}: RLineChartProps) {
|
|
57
|
+
return (
|
|
58
|
+
<div className={`w-full ${className}`} style={{ height }}>
|
|
59
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
60
|
+
<LineChart data={data} margin={{ top: 10, right: 10, left: 0, bottom: 10 }}>
|
|
61
|
+
{showGrid && (
|
|
62
|
+
<CartesianGrid stroke="#f1f5f9" strokeDasharray="0" />
|
|
63
|
+
)}
|
|
64
|
+
<XAxis
|
|
65
|
+
dataKey={xAxisKey}
|
|
66
|
+
tickFormatter={xAxisFormatter}
|
|
67
|
+
tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
|
|
68
|
+
tickLine={false}
|
|
69
|
+
axisLine={false}
|
|
70
|
+
/>
|
|
71
|
+
<YAxis
|
|
72
|
+
tickFormatter={yAxisFormatter}
|
|
73
|
+
tick={{ fontSize: 11, fontWeight: 600, fill: "#94a3b8" }}
|
|
74
|
+
tickLine={false}
|
|
75
|
+
axisLine={false}
|
|
76
|
+
/>
|
|
77
|
+
{showTooltip && (
|
|
78
|
+
<Tooltip
|
|
79
|
+
formatter={tooltipFormatter}
|
|
80
|
+
contentStyle={{
|
|
81
|
+
backgroundColor: "#1e293b",
|
|
82
|
+
color: "#fff",
|
|
83
|
+
borderRadius: "8px",
|
|
84
|
+
border: "none",
|
|
85
|
+
}}
|
|
86
|
+
itemStyle={{ color: "#fff" }}
|
|
87
|
+
animationDuration={1000}
|
|
88
|
+
/>
|
|
89
|
+
)}
|
|
90
|
+
{showLegend && (
|
|
91
|
+
<Legend
|
|
92
|
+
verticalAlign={legendPosition}
|
|
93
|
+
wrapperStyle={{ fontSize: "12px", fontWeight: 700 }}
|
|
94
|
+
iconType="circle"
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
{series.map((s, idx) => (
|
|
98
|
+
<Line
|
|
99
|
+
key={s.dataKey}
|
|
100
|
+
type={s.type || "monotone"}
|
|
101
|
+
dataKey={s.dataKey}
|
|
102
|
+
name={s.name || s.dataKey}
|
|
103
|
+
stroke={s.color || colors[idx % colors.length]}
|
|
104
|
+
strokeWidth={s.strokeWidth || 2}
|
|
105
|
+
strokeDasharray={s.dashed ? "5 5" : undefined}
|
|
106
|
+
dot={s.showDot ? { r: 4 } : false}
|
|
107
|
+
activeDot={{ r: 6 }}
|
|
108
|
+
isAnimationActive={true}
|
|
109
|
+
animationDuration={1000}
|
|
110
|
+
/>
|
|
111
|
+
))}
|
|
112
|
+
</LineChart>
|
|
113
|
+
</ResponsiveContainer>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import React, { useState, useEffect, useCallback, useRef } from "react";
|
|
3
|
+
import { Modal, Button, Input, Pagination, EmptyState, ModalFooter } from "@apptimate/ui";
|
|
4
|
+
import { Search, User, Phone, Mail, MapPin, X } from "lucide-react";
|
|
5
|
+
import toast from "react-hot-toast";
|
|
6
|
+
import { sendRequest, IApiResponse } from "@apptimate/core-lib";
|
|
7
|
+
|
|
8
|
+
export interface SelectedCustomer {
|
|
9
|
+
id: number;
|
|
10
|
+
customer_name: string;
|
|
11
|
+
customer_code?: string;
|
|
12
|
+
phone?: string;
|
|
13
|
+
email?: string;
|
|
14
|
+
address?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CustomerSelectorProps {
|
|
18
|
+
/** Currently selected customer ID */
|
|
19
|
+
value?: number | null;
|
|
20
|
+
/** Callback with full customer object on select */
|
|
21
|
+
onChange: (customer: SelectedCustomer | null) => void;
|
|
22
|
+
/** API base URL */
|
|
23
|
+
apiBase?: string;
|
|
24
|
+
/** API endpoint for listing customers. Default: /api/sales/customers */
|
|
25
|
+
apiPath?: string;
|
|
26
|
+
/** Whether the selector is disabled */
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
/** Label text */
|
|
29
|
+
label?: string;
|
|
30
|
+
/** Placeholder text */
|
|
31
|
+
placeholder?: string;
|
|
32
|
+
/** Required */
|
|
33
|
+
required?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function CustomerSelectorComponent({
|
|
37
|
+
value,
|
|
38
|
+
onChange,
|
|
39
|
+
apiBase = process.env.NEXT_PUBLIC_API_URL || "",
|
|
40
|
+
apiPath = "/api/sales/customers",
|
|
41
|
+
disabled = false,
|
|
42
|
+
label = "Customer",
|
|
43
|
+
placeholder = "Search customers...",
|
|
44
|
+
required = false,
|
|
45
|
+
}: CustomerSelectorProps) {
|
|
46
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
47
|
+
const [search, setSearch] = useState("");
|
|
48
|
+
const [dSearch, setDSearch] = useState("");
|
|
49
|
+
const [customers, setCustomers] = useState<any[]>([]);
|
|
50
|
+
const [loading, setLoading] = useState(false);
|
|
51
|
+
const [page, setPage] = useState(1);
|
|
52
|
+
const [total, setTotal] = useState(0);
|
|
53
|
+
const [selected, setSelected] = useState<SelectedCustomer | null>(null);
|
|
54
|
+
const searchRef = useRef<HTMLInputElement>(null);
|
|
55
|
+
|
|
56
|
+
useEffect(() => { const t = setTimeout(() => { setDSearch(search); setPage(1); }, 350); return () => clearTimeout(t); }, [search]);
|
|
57
|
+
|
|
58
|
+
const fetchCustomers = useCallback(async () => {
|
|
59
|
+
setLoading(true);
|
|
60
|
+
try {
|
|
61
|
+
const qs = new URLSearchParams({ page: String(page), per_page: "15", ...(dSearch ? { search: dSearch } : {}) }).toString();
|
|
62
|
+
const r = await sendRequest({ url: `${apiBase}${apiPath}?${qs}`, method: "GET" });
|
|
63
|
+
const data = r.responseData as IApiResponse;
|
|
64
|
+
if (data.is_success) { setCustomers(data.result?.data || []); setTotal(data.result?.total ?? 0); }
|
|
65
|
+
} catch (e: any) { toast.error(e.message); } finally { setLoading(false); }
|
|
66
|
+
}, [page, dSearch, apiBase, apiPath]);
|
|
67
|
+
|
|
68
|
+
useEffect(() => { if (isOpen) fetchCustomers(); }, [isOpen, fetchCustomers]);
|
|
69
|
+
useEffect(() => { if (isOpen) setTimeout(() => searchRef.current?.focus(), 100); }, [isOpen]);
|
|
70
|
+
|
|
71
|
+
const handleSelect = (c: any) => {
|
|
72
|
+
const sel: SelectedCustomer = {
|
|
73
|
+
id: c.id,
|
|
74
|
+
customer_name: c.customer_name || c.name,
|
|
75
|
+
customer_code: c.customer_code || c.code,
|
|
76
|
+
phone: c.phone,
|
|
77
|
+
email: c.email,
|
|
78
|
+
address: c.address,
|
|
79
|
+
};
|
|
80
|
+
setSelected(sel);
|
|
81
|
+
onChange(sel);
|
|
82
|
+
setIsOpen(false);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleClear = () => {
|
|
86
|
+
setSelected(null);
|
|
87
|
+
onChange(null);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div>
|
|
92
|
+
<label className="text-xs font-bold text-gray-500 uppercase mb-1 block">{label}{required && <span className="text-red-500 ml-0.5">*</span>}</label>
|
|
93
|
+
|
|
94
|
+
{/* Display */}
|
|
95
|
+
{selected ? (
|
|
96
|
+
<div className="flex items-center gap-2 border border-gray-200 rounded-lg px-3 py-2 bg-white">
|
|
97
|
+
<div className="flex-1 min-w-0">
|
|
98
|
+
<p className="text-sm font-bold text-gray-800 truncate">{selected.customer_name}</p>
|
|
99
|
+
<p className="text-xs text-gray-500">{[selected.customer_code, selected.phone].filter(Boolean).join(" • ")}</p>
|
|
100
|
+
</div>
|
|
101
|
+
{!disabled && <button onClick={handleClear} className="p-1 rounded hover:bg-gray-100"><X size={14} className="text-gray-400" /></button>}
|
|
102
|
+
</div>
|
|
103
|
+
) : (
|
|
104
|
+
<button
|
|
105
|
+
onClick={() => !disabled && setIsOpen(true)}
|
|
106
|
+
className={`w-full text-left border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-400 hover:border-blue-300 transition-colors ${disabled ? "bg-gray-50 cursor-not-allowed" : "bg-white cursor-pointer"}`}
|
|
107
|
+
>
|
|
108
|
+
<span className="flex items-center gap-2"><Search size={14} />{placeholder}</span>
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
|
|
112
|
+
{/* Selector Modal */}
|
|
113
|
+
<Modal isOpen={isOpen} onClose={() => setIsOpen(false)} title={`Select ${label}`} size="lg">
|
|
114
|
+
<div className="p-1">
|
|
115
|
+
<div className="relative mb-4">
|
|
116
|
+
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400" />
|
|
117
|
+
<input
|
|
118
|
+
ref={searchRef}
|
|
119
|
+
type="text"
|
|
120
|
+
value={search}
|
|
121
|
+
onChange={e => setSearch(e.target.value)}
|
|
122
|
+
placeholder="Search by name, code, phone, or email..."
|
|
123
|
+
className="w-full pl-9 pr-4 py-2.5 border border-gray-200 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-200 focus:border-blue-300"
|
|
124
|
+
/>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
{loading ? (
|
|
128
|
+
<div className="flex items-center justify-center py-16"><div className="w-6 h-6 border-2 border-blue-400 border-t-transparent rounded-full animate-spin" /></div>
|
|
129
|
+
) : customers.length === 0 ? (
|
|
130
|
+
<div className="py-12 text-center"><EmptyState message="No customers found." /></div>
|
|
131
|
+
) : (
|
|
132
|
+
<div className="space-y-1.5 max-h-[50vh] overflow-y-auto">
|
|
133
|
+
{customers.map((c: any) => (
|
|
134
|
+
<button
|
|
135
|
+
key={c.id}
|
|
136
|
+
onClick={() => handleSelect(c)}
|
|
137
|
+
className="w-full text-left p-3 rounded-xl hover:bg-blue-50 border border-transparent hover:border-blue-200 transition-all flex items-start gap-3"
|
|
138
|
+
>
|
|
139
|
+
<div className="p-2 rounded-xl bg-gray-100 text-gray-500"><User size={16} /></div>
|
|
140
|
+
<div className="flex-1 min-w-0">
|
|
141
|
+
<p className="text-sm font-bold text-gray-800">{c.customer_name || c.name}</p>
|
|
142
|
+
<div className="flex items-center gap-3 mt-1 text-xs text-gray-500">
|
|
143
|
+
{c.customer_code && <span className="bg-gray-100 px-1.5 py-0.5 rounded font-mono">{c.customer_code}</span>}
|
|
144
|
+
{c.phone && <span className="flex items-center gap-1"><Phone size={10} />{c.phone}</span>}
|
|
145
|
+
{c.email && <span className="flex items-center gap-1"><Mail size={10} />{c.email}</span>}
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</button>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{!loading && customers.length > 0 && total > 15 && (
|
|
154
|
+
<div className="mt-3 pt-3 border-t border-gray-100">
|
|
155
|
+
<Pagination currentPage={page} totalItems={total} itemsPerPage={15} onPageChange={setPage} />
|
|
156
|
+
</div>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
<ModalFooter><Button variant="soft" color="default" onClick={() => setIsOpen(false)}>Cancel</Button></ModalFooter>
|
|
160
|
+
</Modal>
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
}
|