Jelajahi Sumber

feat: csv bulk import

Timothy J. Baek 1 tahun lalu
induk
melakukan
20a6dbbe65
2 mengubah file dengan 108 tambahan dan 83 penghapusan
  1. 0 70
      backend/apps/web/routers/auths.py
  2. 108 13
      src/lib/components/admin/AddUserModal.svelte

+ 0 - 70
backend/apps/web/routers/auths.py

@@ -253,76 +253,6 @@ async def add_user(form_data: AddUserForm, user=Depends(get_admin_user)):
         raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
 
 
-@router.post("/add/import", response_model=SigninResponse)
-async def add_user_csv_import(
-    file: UploadFile = File(...), user=Depends(get_admin_user)
-):
-    try:
-
-        # Check if the file is a CSV file
-        if file.filename.endswith(".csv"):
-            # Read the contents of the CSV file
-            contents = await file.read()
-
-            # Decode the contents from bytes to string
-            decoded_content = contents.decode("utf-8")
-
-            # Split the CSV content into lines
-            csv_lines = decoded_content.split("\n")
-
-            # Parse CSV
-            csv_data = []
-            csv_reader = csv.reader(csv_lines)
-            for row in csv_reader:
-                csv_data.append(row)
-
-            # Print the CSV data
-            for row in csv_data:
-                print(row)
-
-            return {"message": "CSV file uploaded successfully."}
-        else:
-            raise "File must be a CSV."
-    except Exception as err:
-        raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
-
-    # if not validate_email_format(form_data.email.lower()):
-    #     raise HTTPException(
-    #         status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.INVALID_EMAIL_FORMAT
-    #     )
-
-    # if Users.get_user_by_email(form_data.email.lower()):
-    #     raise HTTPException(400, detail=ERROR_MESSAGES.EMAIL_TAKEN)
-
-    # try:
-
-    #     print(form_data)
-    #     hashed = get_password_hash(form_data.password)
-    #     user = Auths.insert_new_auth(
-    #         form_data.email.lower(),
-    #         hashed,
-    #         form_data.name,
-    #         form_data.profile_image_url,
-    #         form_data.role,
-    #     )
-
-    #     if user:
-    #         token = create_token(data={"id": user.id})
-    #         return {
-    #             "token": token,
-    #             "token_type": "Bearer",
-    #             "id": user.id,
-    #             "email": user.email,
-    #             "name": user.name,
-    #             "role": user.role,
-    #             "profile_image_url": user.profile_image_url,
-    #         }
-    #     else:
-    #         raise HTTPException(500, detail=ERROR_MESSAGES.CREATE_USER_ERROR)
-    # except Exception as err:
-    #     raise HTTPException(500, detail=ERROR_MESSAGES.DEFAULT(err))
-
-
 ############################
 # ToggleSignUp
 ############################

+ 108 - 13
src/lib/components/admin/AddUserModal.svelte

@@ -12,6 +12,7 @@
 
 	export let show = false;
 
+	let loading = false;
 	let tab = '';
 	let inputFiles;
 
@@ -32,19 +33,82 @@
 	}
 
 	const submitHandler = async () => {
-		const res = await addUser(
-			localStorage.token,
-			_user.name,
-			_user.email,
-			_user.password,
-			_user.role
-		).catch((error) => {
-			toast.error(error);
-		});
-
-		if (res) {
+		const stopLoading = () => {
 			dispatch('save');
-			show = false;
+			loading = false;
+		};
+
+		if (tab === '') {
+			loading = true;
+
+			const res = await addUser(
+				localStorage.token,
+				_user.name,
+				_user.email,
+				_user.password,
+				_user.role
+			).catch((error) => {
+				toast.error(error);
+			});
+
+			if (res) {
+				stopLoading();
+				show = false;
+			}
+		} else {
+			if (inputFiles) {
+				loading = true;
+
+				const file = inputFiles[0];
+				const reader = new FileReader();
+
+				reader.onload = async (e) => {
+					const csv = e.target.result;
+					const rows = csv.split('\n');
+
+					let userCount = 0;
+
+					for (const [idx, row] of rows.entries()) {
+						const columns = row.split(',').map((col) => col.trim());
+						console.log(idx, columns);
+
+						if (idx > 0) {
+							if (columns.length === 4 && ['admin', 'user', 'pending'].includes(columns[3])) {
+								const res = await addUser(
+									localStorage.token,
+									columns[0],
+									columns[1],
+									columns[2],
+									columns[3]
+								).catch((error) => {
+									toast.error(`Row ${idx + 1}: ${error}`);
+									return null;
+								});
+
+								if (res) {
+									userCount = userCount + 1;
+								}
+							} else {
+								toast.error(`Row ${idx + 1}: invalid format.`);
+							}
+						}
+					}
+
+					toast.success(`Successfully imported ${userCount} users`);
+					inputFiles = null;
+					const uploadInputElement = document.getElementById('upload-user-csv-input');
+
+					if (uploadInputElement) {
+						uploadInputElement.value = null;
+					}
+
+					stopLoading();
+				};
+
+				reader.readAsText(file);
+			} else {
+				toast.error(`File not found.`);
+			}
 		}
 	};
 </script>
@@ -204,10 +268,41 @@
 
 					<div class="flex justify-end pt-3 text-sm font-medium">
 						<button
-							class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg"
+							class=" px-4 py-2 bg-emerald-700 hover:bg-emerald-800 text-gray-100 transition rounded-lg flex flex-row space-x-1 items-center {loading
+								? ' cursor-not-allowed'
+								: ''}"
 							type="submit"
+							disabled={loading}
 						>
 							{$i18n.t('Submit')}
+
+							{#if loading}
+								<div class="ml-2 self-center">
+									<svg
+										class=" w-4 h-4"
+										viewBox="0 0 24 24"
+										fill="currentColor"
+										xmlns="http://www.w3.org/2000/svg"
+										><style>
+											.spinner_ajPY {
+												transform-origin: center;
+												animation: spinner_AtaB 0.75s infinite linear;
+											}
+											@keyframes spinner_AtaB {
+												100% {
+													transform: rotate(360deg);
+												}
+											}
+										</style><path
+											d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
+											opacity=".25"
+										/><path
+											d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
+											class="spinner_ajPY"
+										/></svg
+									>
+								</div>
+							{/if}
 						</button>
 					</div>
 				</form>