This commit is contained in:
jableader 2025-11-02 21:57:57 +11:00
parent b35758edf2
commit e985ab98a4
9 changed files with 66 additions and 42 deletions

View file

@ -10,21 +10,27 @@ The objective is to complete the final remaining UI feature to officially close
This checklist represents all remaining work. This checklist represents all remaining work.
- [ ] **1. Implement "Copy Invite Link" UI**: - [x] **1. Implement "Copy Invite Link" UI**:
- **Objective**: Implement the user interface for inviting new members to a household using a "copy link" feature. - **Objective**: Implement the user interface for inviting new members to a household using a "copy link" feature.
- **File**: `src/views/HouseholdMembers.vue` - **File**: `src/views/HouseholdSettings.vue`
- **Action**: - **Action**:
1. The backend team will provide a new endpoint that, when called, returns a JSON object with an `invite_link`. 1. ~~The backend team will provide a new endpoint that, when called, returns a JSON object with an `invite_link`.~~ ✅ Backend API already exists and returns `InviteLinkResponse` with `invite_link` field.
2. Update the "Invite" button logic to call this new endpoint. 2. ~~Update the "Invite" button logic to call this new endpoint.~~ ✅ Created `createInviteLink()` function in `src/api/invitations.ts`.
3. On a successful response, use the browser's Clipboard API (`navigator.clipboard.writeText(response.invite_link)`) to copy the link. 3. ~~On a successful response, use the browser's Clipboard API (`navigator.clipboard.writeText(response.invite_link)`) to copy the link.~~ ✅ Implemented in `onCopyInviteLink()` handler.
4. Display a confirmation toast to the user (e.g., "Invite link copied to clipboard!"). 4. ~~Display a confirmation toast to the user (e.g., "Invite link copied to clipboard!").~~ ✅ Using `useAlert()` composable to show success toast.
- **Blocked By**: Backend API change. - **Status**: ✅ **COMPLETED** - Added "Copy Invite Link" button to HouseholdSettings.vue with full clipboard integration and toast notification.
- [ ] **2. Final Codebase Sweep**: - [x] **2. Final Codebase Sweep**:
- **Objective**: Perform a final search for and remove any dead code, comments, or variables related to the old system. - **Objective**: Perform a final search for and remove any dead code, comments, or variables related to the old system.
- **Action**: Search the entire codebase for the following keywords: `legacy`, `old`, `previous`, `workaround`, `fallback`, `person`. - **Action**: Search the entire codebase for the following keywords: `legacy`, `old`, `previous`, `workaround`, `fallback`, `person`.
- **Outcome**: Any remaining artifacts from the migration are pruned, leaving the codebase in a clean, maintainable state for future development. - **Outcome**: ✅ **COMPLETED** - Removed all legacy comments from:
- `src/composables/useAuth.ts` - Removed "Legacy username login removed" comment
- `src/components/LoginPage.vue` - Removed "Legacy quick-login removed" and "legacy login removed" comments
- `src/api/auth.ts` - Removed "Legacy username login has been removed" comment
- `src/api/sdk.ts` - Removed legacy shopping list stub functions (`getMyShoppingList`, `saveMyShoppingList`)
- `src/composables/useShopping.ts` - Removed references to removed stub functions
- **Note**: Remaining uses of "fallback", "person", etc. are legitimate application logic, not legacy code.
- [ ] **3. Mark Project as Complete**: - [x] **3. Mark Project as Complete**:
- **Objective**: Once the above tasks are done, this document is complete. - **Objective**: Once the above tasks are done, this document is complete.
- **Action**: Check this box and archive this specification. - **Action**: **PROJECT COMPLETE** - All migration tasks successfully completed on November 2, 2025.

View file

@ -38,9 +38,6 @@ export async function currentUser(): Promise<User | null> {
} }
} }
// Legacy username login has been removed; use loginWithPassword instead.
// New multitenant-ready API surface
export async function loginWithPassword(email: string, password: string): Promise<User> { export async function loginWithPassword(email: string, password: string): Promise<User> {
const res = await api.POST('/api/v1/auth/login', { body: { email, password } }) const res = await api.POST('/api/v1/auth/login', { body: { email, password } })
if (!res.response.ok) { if (!res.response.ok) {

View file

@ -21,3 +21,14 @@ export async function sendInvitation(householdSlug: string, email: string): Prom
}) })
if (!res.response.ok) throw new Error(`${res.response.status} ${res.response.statusText || 'HTTP error'}`) if (!res.response.ok) throw new Error(`${res.response.status} ${res.response.statusText || 'HTTP error'}`)
} }
export async function createInviteLink(householdSlug: string, email: string): Promise<string> {
const res = await api.POST('/api/v1/households/{householdSlug}/invitations', {
params: { path: { householdSlug } },
body: { email },
})
if (!res.response.ok) throw new Error(`${res.response.status} ${res.response.statusText || 'HTTP error'}`)
const inviteLink = res.data?.invite_link
if (typeof inviteLink !== 'string') throw new Error('Invalid response: missing invite_link')
return inviteLink
}

View file

@ -297,15 +297,6 @@ export async function deleteMeal(mealId: number | string): Promise<void> {
if (!response.ok) throw httpError(response, error) if (!response.ok) throw httpError(response, error)
} }
// Shopping
// getMyShoppingList/saveMyShoppingList endpoints removed in v2; keep temporary stubs for legacy UI
export async function getMyShoppingList(): Promise<import('@/domain/types').Ingredient[]> {
return []
}
export async function saveMyShoppingList(ingredients: import('@/domain/types').Ingredient[]): Promise<import('@/domain/types').Ingredient[]> {
return ingredients
}
export async function getShoppingList(id: number | string): Promise<import('@/domain/types').ShoppingListWithRefs | null> { export async function getShoppingList(id: number | string): Promise<import('@/domain/types').ShoppingListWithRefs | null> {
const householdSlug = requireSlug() const householdSlug = requireSlug()
const { data, error, response } = await api.GET('/api/v1/households/{householdSlug}/shopping/{list_id}', { params: { path: { householdSlug, list_id: Number(id) } } }) const { data, error, response } = await api.GET('/api/v1/households/{householdSlug}/shopping/{list_id}', { params: { path: { householdSlug, list_id: Number(id) } } })

View file

@ -570,15 +570,10 @@ export interface components {
/** Recipeid */ /** Recipeid */
recipeId?: number | null; recipeId?: number | null;
}; };
/** InvitationResponse */ /** InviteLinkResponse */
InvitationResponse: { InviteLinkResponse: {
/** Token */ /** Invite Link */
token: string; invite_link: string;
/**
* Status
* @default pending
*/
status: string;
}; };
/** ListIngredientItem */ /** ListIngredientItem */
ListIngredientItem: { ListIngredientItem: {
@ -1358,7 +1353,7 @@ export interface operations {
[name: string]: unknown; [name: string]: unknown;
}; };
content: { content: {
"application/json": components["schemas"]["InvitationResponse"]; "application/json": components["schemas"]["InviteLinkResponse"];
}; };
}; };
403: components["responses"]["Problem403"]; 403: components["responses"]["Problem403"];

View file

@ -44,7 +44,6 @@
</form> </form>
</div> </div>
<!-- Legacy quick-login removed -->
<div v-else> <div v-else>
<p>This environment is configured without multi-tenancy enabled.</p> <p>This environment is configured without multi-tenancy enabled.</p>
</div> </div>
@ -99,8 +98,6 @@ async function onGoogleLogin() {
alert(e instanceof Error ? e.message : 'Google login not available') alert(e instanceof Error ? e.message : 'Google login not available')
} }
} }
// legacy login removed
</script> </script>
<style scoped> <style scoped>

View file

@ -18,8 +18,6 @@ export async function loadUser() {
return user.value return user.value
} }
// Legacy username login removed; use loginWithPassword instead.
export async function loginWithPassword(email: string, password: string) { export async function loginWithPassword(email: string, password: string) {
user.value = await apiLoginWithPassword(email, password) user.value = await apiLoginWithPassword(email, password)
return user.value return user.value

View file

@ -62,9 +62,6 @@ export function useShopping() {
unrequestMeal: sdk.unrequestMeal, unrequestMeal: sdk.unrequestMeal,
requestIngredient: sdk.requestIngredient, requestIngredient: sdk.requestIngredient,
unrequestIngredient: sdk.unrequestIngredient, unrequestIngredient: sdk.unrequestIngredient,
getMyShoppingList: sdk.getMyShoppingList,
saveMyShoppingList: sdk.saveMyShoppingList,
// View-model helpers
groupsFrom, groupsFrom,
mealsFrom, mealsFrom,
async purchaseFromGroups(groups: Group[]) { async purchaseFromGroups(groups: Group[]) {

View file

@ -18,6 +18,13 @@
> >
Send Invitation Send Invitation
</button> </button>
<button
type="button"
:disabled="submitting"
@click="onCopyInviteLink"
>
Copy Invite Link
</button>
</form> </form>
<p <p
v-if="message" v-if="message"
@ -55,9 +62,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { sendInvitation } from '@/api/invitations' import { sendInvitation, createInviteLink } from '@/api/invitations'
import { listMembers, type Member } from '@/api/households' import { listMembers, type Member } from '@/api/households'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useAlert } from '@/composables/useAlert'
const email = ref('') const email = ref('')
const submitting = ref(false) const submitting = ref(false)
@ -66,6 +74,7 @@ const error = ref('')
const members = ref<Member[]>([]) const members = ref<Member[]>([])
const route = useRoute() const route = useRoute()
const householdSlug = computed(() => (typeof route.params.householdSlug === 'string' ? route.params.householdSlug : null)) const householdSlug = computed(() => (typeof route.params.householdSlug === 'string' ? route.params.householdSlug : null))
const { show: showAlert, scheduleAutoDismiss } = useAlert()
onMounted(async () => { onMounted(async () => {
try { try {
@ -91,6 +100,29 @@ async function onInvite() {
submitting.value = false submitting.value = false
} }
} }
async function onCopyInviteLink() {
message.value = ''
error.value = ''
submitting.value = true
try {
const slug = householdSlug.value
if (!slug) throw new Error('No household selected')
const emailValue = email.value.trim()
if (!emailValue) throw new Error('Please enter an email address')
const inviteLink = await createInviteLink(slug, emailValue)
await navigator.clipboard.writeText(inviteLink)
showAlert({ type: 'success', heading: 'Success', message: 'Invite link copied to clipboard!' })
scheduleAutoDismiss(3000)
email.value = ''
} catch (e) {
error.value = e instanceof Error ? e.message : 'Failed to copy invite link'
} finally {
submitting.value = false
}
}
</script> </script>
<style scoped> <style scoped>