1
1
// No need to import React with modern JSX transform
2
- import { useState , useEffect } from "react" ;
3
- import type { Schema } from "../../amplify/data/resource" ;
4
2
import { Button } from "../components/ui/button" ;
5
3
import { AccountCard } from "../components/account-card" ;
6
4
import { client } from "../lib/amplify-client" ;
7
5
import { useNavigate } from "react-router" ;
6
+ import { parse } from "csv-parse/browser/esm" ;
7
+ import { CSVImportDialog } from "../components/csv-import-dialog" ;
8
+ import type { Route } from "./+types/accounts" ;
8
9
9
10
export function meta ( ) {
10
11
return [
@@ -13,62 +14,171 @@ export function meta() {
13
14
] ;
14
15
}
15
16
16
- type Account = Schema [ "Account" ] [ "type" ] ;
17
+ export async function clientAction ( { request } : { request : Request } ) {
18
+ const formData = await request . formData ( ) ;
19
+ const csvFile = formData . get ( "csvFile" ) as File ;
17
20
18
- export default function Accounts ( ) {
19
- const [ accounts , setAccounts ] = useState < Account [ ] > ( [ ] ) ;
20
- const [ loading , setLoading ] = useState ( true ) ;
21
- const [ error , setError ] = useState < Error | null > ( null ) ;
21
+ if ( ! csvFile ) {
22
+ return { error : "CSVファイルが必要です" } ;
23
+ }
24
+
25
+ try {
26
+ const text = await csvFile . text ( ) ;
27
+ const parseCSV = ( ) => {
28
+ return new Promise < Array < Record < string , string > > > ( ( resolve , reject ) => {
29
+ parse (
30
+ text ,
31
+ {
32
+ columns : true ,
33
+ skip_empty_lines : true ,
34
+ trim : true ,
35
+ } ,
36
+ ( err , records ) => {
37
+ if ( err ) {
38
+ reject ( err ) ;
39
+ } else {
40
+ resolve ( records ) ;
41
+ }
42
+ } ,
43
+ ) ;
44
+ } ) ;
45
+ } ;
46
+
47
+ const records = await parseCSV ( ) ;
48
+
49
+ const results = {
50
+ success : 0 ,
51
+ errors : [ ] as string [ ] ,
52
+ } ;
53
+
54
+ for ( let i = 0 ; i < records . length ; i ++ ) {
55
+ const record = records [ i ] ;
56
+
57
+ if (
58
+ ! record . name ||
59
+ ! record . email ||
60
+ ! record . organizationLine ||
61
+ ! record . residence
62
+ ) {
63
+ results . errors . push (
64
+ `行 ${ i + 1 } : 名前、メール、組織、居住地は必須です` ,
65
+ ) ;
66
+ continue ;
67
+ }
68
+
69
+ try {
70
+ const { errors } = await client . models . Account . create ( {
71
+ name : record . name ,
72
+ email : record . email ,
73
+ photo : record . photo || undefined ,
74
+ organizationLine : record . organizationLine ,
75
+ residence : record . residence ,
76
+ } ) ;
77
+
78
+ if ( errors ) {
79
+ results . errors . push (
80
+ `行 ${ i + 1 } : ${ errors . map ( ( err ) => err . message ) . join ( ", " ) } ` ,
81
+ ) ;
82
+ } else {
83
+ results . success ++ ;
84
+ }
85
+ } catch ( error ) {
86
+ results . errors . push (
87
+ `行 ${ i + 1 } : ${ error instanceof Error ? error . message : "不明なエラー" } ` ,
88
+ ) ;
89
+ }
90
+ }
91
+
92
+ return {
93
+ results,
94
+ } ;
95
+ } catch ( error ) {
96
+ return {
97
+ error : `CSVの処理中にエラーが発生しました: ${ error instanceof Error ? error . message : "不明なエラー" } ` ,
98
+ } ;
99
+ }
100
+ }
101
+
102
+ export async function clientLoader ( ) {
103
+ try {
104
+ const { data } = await client . models . Account . list ( ) ;
105
+ return { accounts : data } ;
106
+ } catch ( err ) {
107
+ console . error ( "Error fetching accounts:" , err ) ;
108
+ return {
109
+ accounts : [ ] ,
110
+ error : err instanceof Error ? err . message : "Unknown error occurred" ,
111
+ } ;
112
+ }
113
+ }
114
+
115
+ export default function Accounts ( {
116
+ loaderData,
117
+ actionData,
118
+ } : Route . ComponentProps ) {
22
119
const navigate = useNavigate ( ) ;
23
120
const handleAccountClick = ( accountId : string ) => {
24
121
navigate ( `/accounts/${ accountId } ` ) ;
25
122
} ;
26
- const fetchAccounts = async ( ) => {
27
- try {
28
- setLoading ( true ) ;
29
- const { data } = await client . models . Account . list ( ) ;
30
- setAccounts ( data ) ;
31
- } catch ( err ) {
32
- console . error ( "Error fetching accounts:" , err ) ;
33
- setError (
34
- err instanceof Error ? err : new Error ( "Unknown error occurred" ) ,
35
- ) ;
36
- } finally {
37
- setLoading ( false ) ;
38
- }
39
- } ;
40
-
41
- useEffect ( ( ) => {
42
- fetchAccounts ( ) ;
43
- } , [ ] ) ;
44
123
45
- // handleAccountCreated removed as it's no longer needed
124
+ const { accounts , error } = loaderData ;
46
125
47
126
return (
48
127
< div className = "container mx-auto p-4" >
49
128
< div className = "flex justify-between items-center mb-6" >
50
129
< h1 className = "text-2xl font-bold" > Accounts</ h1 >
51
- < Button onClick = { ( ) => navigate ( "/accounts/new" ) } > Add Account</ Button >
130
+ < div className = "flex gap-2" >
131
+ < CSVImportDialog
132
+ title = "アカウントインポート"
133
+ description = "CSVファイルからアカウントをインポートします。"
134
+ headers = { [
135
+ { name : "name" , required : true } ,
136
+ { name : "email" , required : true } ,
137
+ { name : "photo" , required : false } ,
138
+ { name : "organizationLine" , required : true } ,
139
+ { name : "residence" , required : true } ,
140
+ ] }
141
+ />
142
+ < Button onClick = { ( ) => navigate ( "/accounts/new" ) } > Add Account</ Button >
143
+ </ div >
52
144
</ div >
53
145
54
- { /* Inline form removed */ }
146
+ { loaderData . error && (
147
+ < div className = "bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" >
148
+ エラー: { loaderData . error }
149
+ </ div >
150
+ ) }
151
+
152
+ { actionData ?. results && (
153
+ < div
154
+ className = { `px-4 py-3 rounded mb-4 ${ actionData ?. results ?. errors . length ? "bg-yellow-100 border border-yellow-400 text-yellow-700" : "bg-green-100 border border-green-400 text-green-700" } ` }
155
+ >
156
+ < p >
157
+ { actionData . results . success }
158
+ 件のアカウントが正常にインポートされました。
159
+ </ p >
160
+ { actionData ?. results ?. errors . length && (
161
+ < >
162
+ < p className = "font-bold mt-2" >
163
+ { actionData . results . errors . length } 件のエラーがありました:
164
+ </ p >
165
+ < ul className = "list-disc pl-5 mt-1" >
166
+ { actionData ?. results ?. errors . map ( ( err , idx ) => (
167
+ < li key = { idx } > { err } </ li >
168
+ ) ) }
169
+ </ ul >
170
+ </ >
171
+ ) }
172
+ </ div >
173
+ ) }
55
174
56
175
{ error && (
57
176
< div className = "bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" >
58
177
Error: { error . toString ( ) }
59
178
</ div >
60
179
) }
61
180
62
- { loading ? (
63
- < div className = "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4" >
64
- { [ ...Array ( 4 ) ] . map ( ( _ , index ) => (
65
- < div
66
- key = { index }
67
- className = "h-48 bg-gray-200 rounded-lg animate-pulse"
68
- > </ div >
69
- ) ) }
70
- </ div >
71
- ) : accounts . length === 0 ? (
181
+ { accounts . length === 0 ? (
72
182
< div className = "text-center py-8" >
73
183
< p className = "text-gray-500" >
74
184
No accounts found. Add an account to get started.
0 commit comments