Build a Finance SaaS Platform -19 (Import Function)

Install Package

1
2
npm i react-papaparse
npx shadcn@latest add select

Add Import Card

Add app/(dashboard)/transactions/import-card.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import { useState } from "react";
import {format,parse } from "date-fns"

import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { ImportTable } from "./import-table";
import { convertAmountToMilliunits } from "@/lib/utils";

const dateFormat = "yyyy-MM-dd HH:mm:ss";
const outputFormat = "yyyy-MM-dd";

const requireOption = [
"amount",
"date",
"payee",
];

interface selectedColumnsState {
[key:string]: string | null
};

type Props ={
data: string[][];
onCancel: ()=> void;
onSummit: (data: any) => void;
}

export const ImportCard = ({data, onCancel, onSummit}: Props) => {
const [selectedColumns, setSelectedColumns] = useState<selectedColumnsState>({});

const headers = data[0];
const body = data.slice(1);

const onTableHeadSelectChange = (
columnIndex: number,
value: string | null
) => {
setSelectedColumns((prev) =>{
const newSelectedColumns = {...prev};

for (const key in newSelectedColumns) {
if (newSelectedColumns[key] === value) {
newSelectedColumns[key] = null;
}
};

if(value ==="skip") {
value = null
};

newSelectedColumns[`column_${columnIndex}`] = value;
return newSelectedColumns;
})
};

const progress = Object.values(selectedColumns).filter(Boolean).length;

const handleContinue = () => {
const getColumnIndex = (column: string) => {
return column.split("_")[1];
}

const mappedData = {
headers: headers.map((_header, index) => {
const columnIndex = getColumnIndex(`column_${index}`);
return selectedColumns[`column_${columnIndex}`];
}),
body: body.map((row) => {
const transformeRow = row.map((cell, index) => {
const columnIndex = getColumnIndex(`column_${index}`);
return selectedColumns[`column_${columnIndex}`]?cell :null;
});

return transformeRow.every((item)=> item === null)
? []
: transformeRow;
}).filter((row)=> row.length> 0),
};

const arrayOfData = mappedData.body.map((row) => {
return row.reduce((acc: any, cell, index) => {
const header = mappedData.headers[index];
if (header != null) {
acc[header] = cell;
}

return acc;
},{});
});

const formattedData = arrayOfData.map(( item ) => ({
...item,
amount: convertAmountToMilliunits(parseFloat(item.amount)),
date: format(parse(item.date, dateFormat,new Date()), outputFormat)
}));

onSummit(formattedData);
};

return(
<div className="max-w-screen-2xl mx-auto w-full pb-10 -mt-24">
<Card className="border-none drop-shadow-sm">
<CardHeader className="gap-y-2 lg:flex-row lg:items-center lg:justify-between">
<CardTitle className="text-xl line-clamp-1">
Import Transaction
</CardTitle>
<div className="flex flex-col lg:flex-row gap-y-2 items-center gap-x-2">
<Button onClick={()=>{}} size="sm" className="w-full lg:w-auto">
Cancel
</Button>
<Button
size="sm"
disabled={progress < requireOption.length}
onClick={handleContinue}
className="w-full lg:w-auto"
>
Continue ({progress} / {requireOption.length})
</Button>
</div>
</CardHeader>
<CardContent>
<ImportTable
headers={headers}
body={body}
selectedColumns={selectedColumns}
onTableHeadSelectChange={onTableHeadSelectChange}
/>
</CardContent>
</Card>
</div>
)
}

Add Import Table

Add app/(dashboard)/transactions/import-table.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { TableHeadSelect } from "./table-head-select";

type Props = {
headers: string[];
body: string[][];
selectedColumns: Record<string, string| null>;
onTableHeadSelectChange: (columnIndex: number, value: string | null) => void;
// onTableBodySelectChange: (rowIndex: number, columnIndex: number, value: string) => void;
};

export const ImportTable =({
headers,
body,
selectedColumns,
onTableHeadSelectChange,
}:Props)=>{
return(
<div className="rounded-md border overflow-hidden">
<Table>
<TableHeader className="bg-muted">
<TableRow>
{headers.map((_item, index) =>(
<TableHead key={index}>
<TableHeadSelect
columnIndex={index}
selectedColumns={selectedColumns}
onChange={onTableHeadSelectChange}
/>
</TableHead>
))}
</TableRow>
</TableHeader>
<TableBody>
{body.map((row:string[], index) =>(
<TableRow key={index}>
{row.map((cell,index)=>(
<TableCell key={index}>
{cell}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
)
}

Add Head Select

Add app/(dashboard)/transactions/table-head-select.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { cn } from "@/lib/utils";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"

type Props = {
columnIndex: number;
selectedColumns: Record<string, string | null>;
onChange: (
columnIndex:number,
value:string | null
) => void;
};

const options = [
"amount",
"payee",
"date",
]

export const TableHeadSelect = ({
columnIndex,
selectedColumns,
onChange,
}: Props) => {
const currentSelection = selectedColumns[`column_${columnIndex}`];

return (
<Select
value={currentSelection || ""}
onValueChange={(value) => onChange(columnIndex, value)}
>
<SelectTrigger
className={cn("focus:ring-offset-0 focus:ring-transparent outline-none border-no bg-transparent capitalize", currentSelection && "text-blue-500")}
>
<SelectValue placeholder="Skip" />
</SelectTrigger>
<SelectContent>
<SelectItem value="skip">Skip</SelectItem>
{options.map((option, index)=>{
const disable =
Object.values(selectedColumns).includes(option) &&
selectedColumns[`column_${columnIndex}`] !== option;

return (
<SelectItem
key={index}
value={option}
disabled={disable}
className="capitalize"
>
{option}
</SelectItem>
);
})}
</SelectContent>
</Select>
)
}

Add Upload Button

Add app/(dashboard)/transactions/upload-button.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useCSVReader } from "react-papaparse";
import { Upload } from "lucide-react";

import { Button } from "@/components/ui/button";

type Props = {
onUpload: ( result: any ) => void;
};

export default function UploadButton({ onUpload }: Props) {
const { CSVReader } = useCSVReader();

//TODO Add a Paywall

return(
<CSVReader onUploadAccepted = {onUpload}>
{({ getRootProps } : any) => (
<Button
size="sm"
className="w-full lg:w-auto"
{...getRootProps()}
>
<Upload className="size-4 mr-2" />
Import CSV
</Button>
)}
</CSVReader>
)
}

Modify Transaction Page

Modify app/(dashboard)/transactions/page.tsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
...
import { columns } from "./columns";
import UploadButton from "./upload-button";
import { ImportCard } from "./import-card";

enum VARIANTS {
LIST = "LIST",
IMPORT = "IMPORT"
};

const INITIAL_IMPORT_RESULTS = {
data: [],
errors: [],
meta: {},
};
...
const TransactionsPage = ()=> {
const [variant, setVariant] = useState<VARIANTS>(VARIANTS.LIST);
const [importResult, setImportResult] = useState(INITIAL_IMPORT_RESULTS);

const handleImport = (results: typeof INITIAL_IMPORT_RESULTS) => {
setImportResult(results);
setVariant(VARIANTS.IMPORT);
};

const handleCancleImport = () => {
setImportResult(INITIAL_IMPORT_RESULTS);
setVariant(VARIANTS.LIST);
}
...
if (variant === VARIANTS.IMPORT) {
return(
<>
<ImportCard
data={importResult.data}
onCancel={handleCancleImport}
onSummit={()=>{}}
/>
</>
)
}
...
<div className="flex items-center gap-x-2">
<Button onClick={newTransaction.onOpen} size="sm">
<Plus className="size-4 mr-2" />
Add New
</Button>
<UploadButton onUpload={handleImport} />
</div>
...

请我喝杯咖啡吧~

支付宝
微信