From 0893cc5d34b09a3ae8ed349f7a354a1c174f928d Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sat, 25 Jul 2026 12:14:25 +0700 Subject: [PATCH] Refactor user management module: remove timezone field, add address, pob, and dob; update related schemas, services, and tests; simplify list users endpoint to support status filtering --- app/factories/user_profile_factory.py | 1 - app/models/user_profile.py | 6 ++- app/routers/auth.py | 21 +++++---- app/routers/users.py | 17 ++----- app/schemas/user.py | 14 ++++-- app/seeders/user_seeder.py | 2 - app/services/user_service.py | 30 ++++++++----- .../history/2026-07-25-modul-user-pengguna.md | 45 +++++++++++++++++++ tests/test_user_api.py | 18 ++------ tests/test_user_service.py | 23 +++------- 10 files changed, 103 insertions(+), 74 deletions(-) create mode 100644 docs/history/2026-07-25-modul-user-pengguna.md diff --git a/app/factories/user_profile_factory.py b/app/factories/user_profile_factory.py index 3c4fb80..6a367b1 100644 --- a/app/factories/user_profile_factory.py +++ b/app/factories/user_profile_factory.py @@ -11,5 +11,4 @@ class UserProfileFactory(Factory): "user_id": None, "full_name": f"{random_string(6).title()} {random_string(8).title()}", "phone": random_phone(), - "timezone": "Asia/Jakarta", } diff --git a/app/models/user_profile.py b/app/models/user_profile.py index 4c9f246..6d3df28 100644 --- a/app/models/user_profile.py +++ b/app/models/user_profile.py @@ -1,7 +1,7 @@ import uuid from datetime import datetime -from sqlalchemy import DateTime, ForeignKey, String +from sqlalchemy import DateTime, ForeignKey, String, Text, Date from sqlalchemy.dialects.mysql import CHAR from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -15,7 +15,9 @@ class UserProfile(Base): user_id: Mapped[str] = mapped_column(CHAR(36), ForeignKey("users.id"), unique=True) full_name: Mapped[str] = mapped_column(String(200)) phone: Mapped[str | None] = mapped_column(String(20), nullable=True) - timezone: Mapped[str | None] = mapped_column(String(50), nullable=True, default="Asia/Jakarta") + address: Mapped[str | None] = mapped_column(Text, nullable=True) + pob: Mapped[str | None] = mapped_column(String(100), nullable=True) + dob: Mapped[datetime | None] = mapped_column(Date, nullable=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) updated_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True, onupdate=datetime.now) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) diff --git a/app/routers/auth.py b/app/routers/auth.py index c80f7e4..7a1529b 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -71,12 +71,19 @@ def register(data: UserCreate, db: Session = Depends(get_db)): db.flush() try: - profile = UserProfile( - user_id=user.id, - full_name=data.full_name, - phone=data.phone, - timezone=data.timezone, - ) + profile_data = { + "user_id": user.id, + "full_name": data.full_name, + } + if data.phone is not None: + profile_data["phone"] = data.phone + if data.address is not None: + profile_data["address"] = data.address + if data.pob is not None: + profile_data["pob"] = data.pob + if data.dob is not None: + profile_data["dob"] = data.dob + profile = UserProfile(**profile_data) db.add(profile) db.commit() except: @@ -139,7 +146,6 @@ def google_login(data: GoogleLoginRequest, db: Session = Depends(get_db)): profile = UserProfile( user_id=user.id, full_name=name, - timezone="Asia/Jakarta", ) db.add(profile) db.commit() @@ -151,7 +157,6 @@ def google_login(data: GoogleLoginRequest, db: Session = Depends(get_db)): profile = UserProfile( user_id=user.id, full_name=name, - timezone="Asia/Jakarta", ) db.add(profile) elif user.profile.full_name != name: diff --git a/app/routers/users.py b/app/routers/users.py index bfc8c5e..ec890a1 100644 --- a/app/routers/users.py +++ b/app/routers/users.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, Depends, Request, Response, status +from fastapi import APIRouter, Depends, Request, status from sqlalchemy.orm import Session from app.database import get_db @@ -21,19 +21,8 @@ router = APIRouter(prefix="/users", tags=["users"]) @router.get("/", response_model=list[UserDetailResponse]) -def list_users( - response: Response, - db: Session = Depends(get_db), - page: int = 1, - limit: int = 10, -): - users, total = get_user_list(db, page, limit) - - response.headers["Pagination-Count"] = str(total) - response.headers["Pagination-Page"] = str(page) - response.headers["Pagination-Limit"] = str(limit) - - return users +def list_users(status: str = None, db: Session = Depends(get_db)): + return get_user_list(db, status=status) @router.get("/{id}", response_model=UserDetailResponse) diff --git a/app/schemas/user.py b/app/schemas/user.py index 7bced86..d2791a3 100644 --- a/app/schemas/user.py +++ b/app/schemas/user.py @@ -1,4 +1,4 @@ -from datetime import datetime +from datetime import date, datetime from pydantic import BaseModel, EmailStr @@ -9,7 +9,9 @@ class UserCreate(BaseModel): password: str full_name: str phone: str | None = None - timezone: str = "Asia/Jakarta" + address: str | None = None + pob: str | None = None + dob: date | None = None class UserUpdate(BaseModel): @@ -18,7 +20,9 @@ class UserUpdate(BaseModel): password: str | None = None full_name: str | None = None phone: str | None = None - timezone: str | None = None + address: str | None = None + pob: str | None = None + dob: date | None = None status: str | None = None @@ -37,7 +41,9 @@ class UserResponse(BaseModel): class UserProfileResponse(BaseModel): full_name: str phone: str | None - timezone: str | None + address: str | None + pob: str | None + dob: date | None class Config: from_attributes = True diff --git a/app/seeders/user_seeder.py b/app/seeders/user_seeder.py index 8ba43f4..864994f 100644 --- a/app/seeders/user_seeder.py +++ b/app/seeders/user_seeder.py @@ -15,7 +15,6 @@ def seed_users(db, count: int = 5, tenant_ids: list[str] | None = None): user_id=user.id, full_name="User Default", phone="081234567890", - timezone="Asia/Jakarta", ) db.add(profile) result.append(user) @@ -40,7 +39,6 @@ def seed_users(db, count: int = 5, tenant_ids: list[str] | None = None): user_id=admin.id, full_name="Pangestu Yoga", phone="081234567890", - timezone="Asia/Jakarta", ) db.add(admin_profile) else: diff --git a/app/services/user_service.py b/app/services/user_service.py index e7d4c6e..4c7e4cd 100644 --- a/app/services/user_service.py +++ b/app/services/user_service.py @@ -9,11 +9,11 @@ from app.schemas.user import UserCreate, UserUpdate from app.security import hash_password -def get_user_list(db: Session, page: int, limit: int): +def get_user_list(db: Session, status: str = None): query = db.query(User).options(selectinload(User.profile)).filter(User.deleted_at.is_(None)) - total = query.count() - users = query.offset((page - 1) * limit).limit(limit).all() - return users, total + if status is not None: + query = query.filter(User.status == status) + return query.order_by(User.created_at.desc()).all() def get_user_by_id(db: Session, id: str): @@ -45,12 +45,18 @@ def create_user(db: Session, data: UserCreate): db.flush() try: - profile = UserProfile( - user_id=user.id, - full_name=data.full_name, - phone=data.phone, - timezone=data.timezone, - ) + profile_data = { + "user_id": user.id, + "full_name": data.full_name, + "phone": data.phone, + } + if data.address is not None: + profile_data["address"] = data.address + if data.pob is not None: + profile_data["pob"] = data.pob + if data.dob is not None: + profile_data["dob"] = data.dob + profile = UserProfile(**profile_data) db.add(profile) db.commit() except: @@ -70,7 +76,7 @@ def update_user(db: Session, id: str, data: UserUpdate): update_data = data.model_dump(exclude_unset=True) - profile_fields = ["full_name", "phone", "timezone"] + profile_fields = ["full_name", "phone", "address", "pob", "dob"] user_fields = {k: v for k, v in update_data.items() if k not in profile_fields} profile_update = {k: v for k, v in update_data.items() if k in profile_fields} @@ -117,4 +123,4 @@ def delete_user(db: Session, id: str): ) user.deleted_at = datetime.now() - db.commit() + db.commit() \ No newline at end of file diff --git a/docs/history/2026-07-25-modul-user-pengguna.md b/docs/history/2026-07-25-modul-user-pengguna.md new file mode 100644 index 0000000..fc2a15c --- /dev/null +++ b/docs/history/2026-07-25-modul-user-pengguna.md @@ -0,0 +1,45 @@ +# Modul User / Pengguna + +**Tanggal:** 2026-07-25 +**Status:** Selesai + +## Tujuan +Membuat modul manajemen pengguna (user) dengan CRUD, mengikuti pola module jenis bisnis (business-types) dan paket (plans). + +## Yang Dikerjakan + +### 1. Simplify List Users Endpoint +- **Sebelum:** Menggunakan pagination (`page`, `limit`) dengan response headers +- **Sesudah:** List tanpa pagination, mendukung filter `status` (opsional) +- Mengikuti pola yang sama seperti business-types dan plans + +### 2. Update UserProfile Model +- **Hapus:** field `timezone` +- **Tambah:** field `address` (Text), `pob` (String 100), `dob` (Date) +- Update schema, service, auth router, factory, seeder + +### 3. Frontend CRUD Users +- Halaman baru `/admin/users` dengan DataTable, filter, create/edit dialog, delete +- Form create/edit: email, username, password, full_name, phone, address, pob, dob + +## File yang Diubah + +| File | Aksi | Detail | +|------|------|--------| +| `api/app/models/user_profile.py` | Diubah | Hapus `timezone`, tambah `address`, `pob`, `dob` | +| `api/app/schemas/user.py` | Diubah | Update `UserCreate`, `UserUpdate`, `UserProfileResponse` | +| `api/app/services/user_service.py` | Diubah | Update `create_user`, `profile_fields` | +| `api/app/routers/auth.py` | Diubah | Hapus `timezone` dari register & google-login | +| `api/app/factories/user_profile_factory.py` | Diubah | Hapus `timezone` | +| `api/app/seeders/user_seeder.py` | Diubah | Hapus `timezone` | +| `api/app/services/user_service.py` | Diubah | Update `get_user_list` (tanpa pagination + filter `status`) | +| `api/app/routers/users.py` | Diubah | Update `GET /` tanpa pagination | +| `api/tests/test_user_service.py` | Diubah | Update test `get_user_list` | +| `api/tests/test_user_api.py` | Diubah | Update test list tanpa pagination | +| `core/app/pages/admin/users/columns.ts` | Dibuat | Column definitions untuk tabel users | +| `core/app/pages/admin/users/index.vue` | Dibuat | Halaman CRUD users (list, create, edit, delete) | +| `core/app/components/AppSidebar.vue` | Diubah | Tambah menu "Pengguna" dengan icon `Users` | + +## Notes +- Migrasi DB: `ALTER TABLE user_profiles DROP COLUMN timezone, ADD COLUMN address TEXT, ADD COLUMN pob VARCHAR(100), ADD COLUMN dob DATE;` +- Menggunakan field `status` yang sudah ada (ACTIVE/INACTIVE) untuk menampilkan status diff --git a/tests/test_user_api.py b/tests/test_user_api.py index 0c0cbde..52f9f20 100644 --- a/tests/test_user_api.py +++ b/tests/test_user_api.py @@ -10,21 +10,6 @@ class TestListUsers: resp = client.get("/v1/users/") assert resp.status_code == 200 assert resp.json() == [] - assert resp.headers.get("Pagination-Count") == "0" - assert resp.headers.get("Pagination-Page") == "1" - assert resp.headers.get("Pagination-Limit") == "10" - - def test_pagination_headers(self, client: TestClient, db_session: Session): - for i in range(5): - db_session.add(User(email=f"u{i}@ex.com", username=f"u{i}", password="x")) - db_session.commit() - - resp = client.get("/v1/users/?page=1&limit=2") - data = resp.json() - assert len(data) == 2 - assert resp.headers["Pagination-Count"] == "5" - assert resp.headers["Pagination-Page"] == "1" - assert resp.headers["Pagination-Limit"] == "2" def test_includes_profile(self, client: TestClient, db_session: Session): user = User(email="prof@ex.com", username="prof", password="x") @@ -123,3 +108,6 @@ class TestDeleteUser: resp = client.delete("/v1/users/nonexistent") assert resp.status_code == 404 assert resp.json() == {"message": "The item does not exist"} + + + diff --git a/tests/test_user_service.py b/tests/test_user_service.py index 5527a15..d8cefbc 100644 --- a/tests/test_user_service.py +++ b/tests/test_user_service.py @@ -17,9 +17,8 @@ from app.services.user_service import ( class TestGetUserList: def test_empty(self, db_session: Session): - users, total = get_user_list(db_session, page=1, limit=10) + users = get_user_list(db_session) assert users == [] - assert total == 0 def test_with_users(self, db_session: Session): for i in range(3): @@ -27,20 +26,9 @@ class TestGetUserList: db_session.add(user) db_session.commit() - users, total = get_user_list(db_session, page=1, limit=10) - assert total == 3 + users = get_user_list(db_session) assert len(users) == 3 - def test_pagination(self, db_session: Session): - for i in range(5): - user = User(email=f"page{i}@ex.com", username=f"page{i}", password="x") - db_session.add(user) - db_session.commit() - - users, total = get_user_list(db_session, page=1, limit=2) - assert total == 5 - assert len(users) == 2 - def test_soft_deleted_excluded(self, db_session: Session): from datetime import datetime @@ -49,8 +37,8 @@ class TestGetUserList: db_session.add_all([u1, u2]) db_session.commit() - users, total = get_user_list(db_session, page=1, limit=10) - assert total == 1 + users = get_user_list(db_session) + assert len(users) == 1 assert users[0].email == "active@ex.com" @@ -199,3 +187,6 @@ class TestDeleteUser: with pytest.raises(HTTPException) as exc: delete_user(db_session, "nonexistent") assert exc.value.status_code == 404 + + +