11// No need to import React with modern JSX transform
2- import { useState , useEffect } from "react" ;
3- import type { Schema } from "../../amplify/data/resource" ;
42import { Button } from "../components/ui/button" ;
53import { AccountCard } from "../components/account-card" ;
64import { client } from "../lib/amplify-client" ;
75import { 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" ;
89
910export function meta ( ) {
1011 return [
@@ -13,62 +14,171 @@ export function meta() {
1314 ] ;
1415}
1516
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 ;
1720
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 ) {
22119 const navigate = useNavigate ( ) ;
23120 const handleAccountClick = ( accountId : string ) => {
24121 navigate ( `/accounts/${ accountId } ` ) ;
25122 } ;
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- } , [ ] ) ;
44123
45- // handleAccountCreated removed as it's no longer needed
124+ const { accounts , error } = loaderData ;
46125
47126 return (
48127 < div className = "container mx-auto p-4" >
49128 < div className = "flex justify-between items-center mb-6" >
50129 < 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 >
52144 </ div >
53145
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+ ) }
55174
56175 { error && (
57176 < div className = "bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4" >
58177 Error: { error . toString ( ) }
59178 </ div >
60179 ) }
61180
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 ? (
72182 < div className = "text-center py-8" >
73183 < p className = "text-gray-500" >
74184 No accounts found. Add an account to get started.
0 commit comments