diff --git a/src/app/api/routes/brands.py b/src/app/api/routes/brands.py index e8582b8..5f9c012 100644 --- a/src/app/api/routes/brands.py +++ b/src/app/api/routes/brands.py @@ -5,7 +5,7 @@ import secrets from typing import Any, Dict, Iterable, List, Mapping, MutableMapping -from fastapi import APIRouter, HTTPException, Request, status +from fastapi import APIRouter, Depends, Form, HTTPException, Request, status from fastapi.responses import HTMLResponse, RedirectResponse from app.core.config import get_settings @@ -16,11 +16,16 @@ BrandNotFound, brand_service, ) +from app.services.profile import ProfileError, ProfileService, profile_service router = APIRouter() +def get_profile_service() -> ProfileService: + return profile_service + + def _render_brand_form( request: Request, *, @@ -53,6 +58,33 @@ def _render_brand_form( ) +def _render_pending_team_column( + request: Request, + brand_slug: str, + *, + feedback_message: str | None = None, + feedback_type: str = "info", + form_state: Dict[str, str] | None = None, + status_code: int = status.HTTP_200_OK, +) -> HTMLResponse: + templates = request.app.state.templates + context = brand_service.get_team_partial_context(brand_slug) + context.update( + { + "request": request, + "feedback_message": feedback_message, + "feedback_type": feedback_type, + "form_state": form_state or {"username": "", "note": ""}, + } + ) + return templates.TemplateResponse( + request, + "components/brand/team_pending_column.html", + context, + status_code=status_code, + ) + + def _empty_member() -> Dict[str, str]: return { "profile_id": "", @@ -438,3 +470,117 @@ async def list_brands(request: Request) -> HTMLResponse: } return templates.TemplateResponse(request, "pages/brand/index.html", context) + +@router.post( + "/brands/{slug}/co-owners", + response_class=HTMLResponse, + name="invite_brand_co_owner", +) +async def invite_brand_co_owner( + request: Request, + slug: str, + username: str = Form(...), + note: str = Form(default=""), + profile_service: ProfileService = Depends(get_profile_service), +) -> HTMLResponse: + try: + profile_view = await profile_service.get_profile(username) + except ProfileError as exc: + return _render_pending_team_column( + request, + slug, + feedback_message=exc.message, + feedback_type="error", + form_state={"username": username, "note": note}, + ) + + try: + brand = brand_service.get_brand(slug) + invited_by = brand.list_owners()[0].full_name if brand.list_owners() else None + brand_service.invite_co_owner( + slug, + profile_id=profile_view.profile.id, + full_name=profile_view.profile.full_name, + username=profile_view.profile.username, + expertise=profile_view.profile.preferred_aroma, + avatar_url=profile_view.profile.avatar_url, + invited_by=invited_by, + ) + except BrandError as exc: + return _render_pending_team_column( + request, + slug, + feedback_message=exc.message, + feedback_type="error", + form_state={"username": username, "note": note}, + ) + + feedback = f"Undangan co-owner dikirim ke {profile_view.profile.full_name}." + if note.strip(): + feedback += " Catatan ikut diteruskan." + + return _render_pending_team_column( + request, + slug, + feedback_message=feedback, + feedback_type="success", + ) + + +@router.post( + "/brands/{slug}/co-owners/{profile_id}/approve", + response_class=HTMLResponse, + name="approve_brand_co_owner", +) +async def approve_brand_co_owner( + request: Request, + slug: str, + profile_id: str, +) -> HTMLResponse: + try: + member = brand_service.approve_co_owner(slug, profile_id) + except BrandError as exc: + return _render_pending_team_column( + request, + slug, + feedback_message=exc.message, + feedback_type="error", + ) + + feedback = f"{member.full_name} sekarang menjadi co-owner aktif." + return _render_pending_team_column( + request, + slug, + feedback_message=feedback, + feedback_type="success", + ) + + +@router.delete( + "/brands/{slug}/co-owners/{profile_id}", + response_class=HTMLResponse, + name="cancel_brand_co_owner", +) +async def cancel_brand_co_owner( + request: Request, + slug: str, + profile_id: str, +) -> HTMLResponse: + try: + member = brand_service.cancel_co_owner_invite(slug, profile_id) + except BrandError as exc: + return _render_pending_team_column( + request, + slug, + feedback_message=exc.message, + feedback_type="error", + ) + + feedback = f"Undangan untuk {member.full_name} dibatalkan." + return _render_pending_team_column( + request, + slug, + feedback_message=feedback, + feedback_type="success", + ) + diff --git a/src/app/services/brands.py b/src/app/services/brands.py index 0273613..e651fc2 100644 --- a/src/app/services/brands.py +++ b/src/app/services/brands.py @@ -500,6 +500,25 @@ def approve_co_owner(self, brand_slug: str, profile_id: str) -> BrandMember: return member raise BrandError("Undangan co-owner tidak ditemukan untuk brand ini.") + def cancel_co_owner_invite(self, brand_slug: str, profile_id: str) -> BrandMember: + brand = self.get_brand(brand_slug) + for index, member in enumerate(brand.members): + if ( + member.profile_id == profile_id + and member.role == "co-owner" + and member.status == "pending" + ): + return brand.members.pop(index) + raise BrandError("Undangan co-owner tidak ditemukan untuk brand ini.") + + def get_team_partial_context(self, brand_slug: str) -> Dict[str, Any]: + brand = self.get_brand(brand_slug) + return { + "brand": brand, + "pending_members": brand.list_pending_members(), + "active_members": brand.list_active_members(), + } + # ------------------------------------------------------------------ # Internal helpers # ------------------------------------------------------------------ diff --git a/src/app/web/templates/components/brand/team_pending_column.html b/src/app/web/templates/components/brand/team_pending_column.html new file mode 100644 index 0000000..4fd9e30 --- /dev/null +++ b/src/app/web/templates/components/brand/team_pending_column.html @@ -0,0 +1,84 @@ +
+

Menunggu Persetujuan

+

+ {% if feedback_message %}{{ feedback_message }}{% endif %} +

+ {% if pending_members %} + + {% else %} +

Belum ada undangan co-owner yang menunggu persetujuan.

+ {% endif %} +
+

Undang Co-owner

+

Masukkan username komunitas untuk meminta persetujuan mereka sebagai co-owner.

+ + + +
+
diff --git a/src/app/web/templates/pages/brand/detail.html b/src/app/web/templates/pages/brand/detail.html index 95d39df..83c8928 100644 --- a/src/app/web/templates/pages/brand/detail.html +++ b/src/app/web/templates/pages/brand/detail.html @@ -144,47 +144,11 @@

Aktif

{% endfor %} -
-

Menunggu Persetujuan

- {% if brand.list_pending_members() %} - - {% else %} -

Belum ada undangan co-owner yang menunggu persetujuan.

- {% endif %} -
-

Undang Co-owner

-

Masukkan username komunitas untuk meminta persetujuan mereka sebagai co-owner.

- - - -
-
+ {% set pending_members = brand.list_pending_members() %} + {% set form_state = {"username": "", "note": ""} %} + {% set feedback_message = None %} + {% set feedback_type = "info" %} + {% include "components/brand/team_pending_column.html" %}