From 140e6c416aaf87c1c16588699fa17fad2704aa78 Mon Sep 17 00:00:00 2001 From: Jackson Roberts Date: Sat, 14 Feb 2026 15:16:06 -0600 Subject: [PATCH] CREDENTIAL TYPE MANAGEMENT WORKS --- src/components/NoResultsMonkey.svelte | 71 ++ src/components/ResultsSpinner.svelte | 49 ++ src/lib/companies.ts | 24 +- src/lib/credentialTypes.ts | 88 +++ src/lib/index.ts | 1 + src/routes/+page.svelte | 2 +- src/routes/admin/+layout.server.ts | 8 + src/routes/admin/+page.svelte | 49 ++ .../admin/credential-types/+page.server.ts | 11 + .../admin/credential-types/+page.svelte | 617 ++++++++++++++++++ src/routes/companies/+page.svelte | 122 +++- src/routes/companies/[id]/+page.server.ts | 52 ++ src/routes/companies/[id]/+page.svelte | 151 +++++ src/routes/company/+page.svelte | 12 + src/routes/company/[id]/+page.server.ts | 17 + src/routes/company/[id]/+page.svelte | 128 +--- src/routes/login/+page.svelte | 2 +- 17 files changed, 1251 insertions(+), 153 deletions(-) create mode 100644 src/components/NoResultsMonkey.svelte create mode 100644 src/components/ResultsSpinner.svelte create mode 100644 src/lib/credentialTypes.ts create mode 100644 src/routes/admin/+layout.server.ts create mode 100644 src/routes/admin/+page.svelte create mode 100644 src/routes/admin/credential-types/+page.server.ts create mode 100644 src/routes/admin/credential-types/+page.svelte create mode 100644 src/routes/companies/[id]/+page.server.ts create mode 100644 src/routes/companies/[id]/+page.svelte diff --git a/src/components/NoResultsMonkey.svelte b/src/components/NoResultsMonkey.svelte new file mode 100644 index 0000000..8c02c1d --- /dev/null +++ b/src/components/NoResultsMonkey.svelte @@ -0,0 +1,71 @@ + + +
+ +
{message}
+
+ + diff --git a/src/components/ResultsSpinner.svelte b/src/components/ResultsSpinner.svelte new file mode 100644 index 0000000..0949870 --- /dev/null +++ b/src/components/ResultsSpinner.svelte @@ -0,0 +1,49 @@ + + + + + diff --git a/src/lib/companies.ts b/src/lib/companies.ts index 7f09e89..6f0e156 100644 --- a/src/lib/companies.ts +++ b/src/lib/companies.ts @@ -2,22 +2,34 @@ import api from "./axios"; export const company = { async fetch(accessToken: string, id: string) { - const company = await api.get(`/v1/company/${id}`, { + const company = await api.get(`/v1/company/companies/${id}`, { headers: { Authorization: `Bearer ${accessToken}`, }, }); return company.data; }, - async fetchMany(accessToken: string, page: number = 1) { - const companies = await api.get("/v1/companies", { - params: { - page, - }, + async fetchMany(accessToken: string, page: number = 1, search?: string) { + const params: Record = { page }; + if (search && search.length > 0) params.search = search; + + const companies = await api.get("/v1/company/companies", { + params, headers: { Authorization: `Bearer ${accessToken}`, }, }); return companies.data; }, + async fetchConfigurations(accessToken: string, id: string) { + const configurations = await api.get( + `/v1/company/companies/${id}/configurations`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }, + ); + return configurations.data; + }, }; diff --git a/src/lib/credentialTypes.ts b/src/lib/credentialTypes.ts new file mode 100644 index 0000000..7d92e12 --- /dev/null +++ b/src/lib/credentialTypes.ts @@ -0,0 +1,88 @@ +import api from "./axios"; + +export interface CredentialTypeField { + id: string; + name: string; + required: boolean; + secure: boolean; + valueType: "plain_text" | "password" | "number" | "email" | "url"; +} + +export interface CredentialType { + id: string; + name: string; + permissionScope: string; + icon?: string; + fields: CredentialTypeField[]; + credentialCount: number; + createdAt: string; + updatedAt: string; +} + +export const credentialType = { + async fetchMany(accessToken: string) { + const response = await api.get("/v1/credential-type", { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async fetch(accessToken: string, identifier: string) { + const response = await api.get(`/v1/credential-type/${identifier}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async create( + accessToken: string, + credentialType: Omit< + CredentialType, + "id" | "credentialCount" | "createdAt" | "updatedAt" + >, + ) { + const response = await api.post("/v1/credential-type", credentialType, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async update( + accessToken: string, + id: string, + updates: Partial< + Omit + >, + ) { + const response = await api.patch(`/v1/credential-type/${id}`, updates, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async delete(accessToken: string, id: string) { + const response = await api.delete(`/v1/credential-type/${id}`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, + + async fetchCredentials(accessToken: string, id: string) { + const response = await api.get(`/v1/credential-type/${id}/credentials`, { + headers: { + Authorization: `Bearer ${accessToken}`, + }, + }); + return response.data; + }, +}; diff --git a/src/lib/index.ts b/src/lib/index.ts index 423d568..9af6e32 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -3,6 +3,7 @@ export * from "./axios"; export * from "./user"; export * from "./companies"; +export * from "./credentialTypes"; /** * @TODO diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 9387f9d..c51776b 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,7 +2,6 @@ import { goto } from "$app/navigation"; function signOut() { - // replace with your auth sign-out logic goto("/logout"); } @@ -18,6 +17,7 @@ Projects Settings Profile + Admin diff --git a/src/routes/admin/+layout.server.ts b/src/routes/admin/+layout.server.ts new file mode 100644 index 0000000..105b8ea --- /dev/null +++ b/src/routes/admin/+layout.server.ts @@ -0,0 +1,8 @@ +import type { LayoutServerLoad } from "./$types"; + +export const load: LayoutServerLoad = async ({ params, parent }) => { + const { session } = await parent(); + return { + accessToken: session.accessToken, + }; +}; diff --git a/src/routes/admin/+page.svelte b/src/routes/admin/+page.svelte new file mode 100644 index 0000000..8439adf --- /dev/null +++ b/src/routes/admin/+page.svelte @@ -0,0 +1,49 @@ + + + + Admin Dashboard — App + + +
+

Admin Dashboard

+ +
+ +
+
+

Administration

+

Manage system settings and configurations.

+
+ +
+
+

Credential Types

+

Create and manage credential type definitions.

+ +
+
+
+ + diff --git a/src/routes/admin/credential-types/+page.server.ts b/src/routes/admin/credential-types/+page.server.ts new file mode 100644 index 0000000..10e1d5f --- /dev/null +++ b/src/routes/admin/credential-types/+page.server.ts @@ -0,0 +1,11 @@ +import type { PageServerLoad } from "./$types"; +import { credentialType } from "$lib/credentialTypes"; + +export const load: PageServerLoad = async ({ params, parent }) => { + const { session } = await parent(); + const response = await credentialType.fetchMany(session.accessToken ?? ""); + return { + credentialTypes: response.data || [], + accessToken: session.accessToken, + }; +}; diff --git a/src/routes/admin/credential-types/+page.svelte b/src/routes/admin/credential-types/+page.svelte new file mode 100644 index 0000000..d897377 --- /dev/null +++ b/src/routes/admin/credential-types/+page.svelte @@ -0,0 +1,617 @@ + + + + Credential Types — Admin + + +
+

Credential Types

+ +
+ +
+ {#if isLoading && !showForm} + + {:else} + {#if error} + + {/if} + + {#if showForm} +
+

{editingId ? "Edit" : "Create"} Credential Type

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+

Credential Fields *

+ {#each formFields as field, index (field.id)} +
+
+ + handleFieldNameBlur(index)} + /> +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ + +
+ {/each} + + +
+ +
+ + +
+
+ {/if} + +
+

Credential Types ({credentialTypes.length})

+ + {#if credentialTypes.length === 0} +

+ No credential types yet. Create one to get started. +

+ {:else} +
+ {#each credentialTypes as ct (ct.id)} +
+ {#if ct.icon} + {ct.name} + {/if} + +

{ct.name}

+

{ct.permissionScope}

+ +
+ Fields ({ct.fields.length}): +
    + {#each ct.fields as field} +
  • + {field.name} + {#if field.secure} + Secure + {/if} + {#if field.required} + Required + {/if} +
  • + {/each} +
+
+ +
+ Used by {ct.credentialCount} credential(s) +
+ +
+ Created: {new Date(ct.createdAt).toLocaleDateString()} + Updated: {new Date(ct.updatedAt).toLocaleDateString()} +
+ +
+ + +
+
+ {/each} +
+ {/if} +
+ {/if} +
+ + diff --git a/src/routes/companies/+page.svelte b/src/routes/companies/+page.svelte index e13951e..9722e0d 100644 --- a/src/routes/companies/+page.svelte +++ b/src/routes/companies/+page.svelte @@ -2,6 +2,7 @@ import { goto } from "$app/navigation"; import { company } from "$lib"; import LoadingSpinner from "$lib/../components/LoadingSpinner.svelte"; + import ResultsSpinner from "$lib/../components/ResultsSpinner.svelte"; import ErrorBoundary from "$lib/../components/ErrorBoundary.svelte"; export let data; @@ -40,12 +41,21 @@ let isLoading = true; let error: string | null = null; let errorDetails: unknown = null; + let isResultsLoading = false; let searchQuery = ""; + let searchTimeout: ReturnType | null = null; const itemsPerPage = 30; - async function loadCompanies(page: number = 1) { - isLoading = true; + async function loadCompanies(page: number = 1, search?: string) { + // If caller provided a `search` argument (even empty string), treat this + // as a results-only refresh and show the inline results loader. Only + // show the full-page loader when `search` is not provided (initial load). + if (search !== undefined) { + isResultsLoading = true; + } else { + isLoading = true; + } error = null; errorDetails = null; try { @@ -53,7 +63,11 @@ throw new Error("No access token available. Please log in again."); } - const response = await company.fetchMany(data.session.accessToken, page); + const response = await company.fetchMany( + data.session.accessToken, + page, + search, + ); if (response && response.data && Array.isArray(response.data)) { companies = response.data; @@ -94,19 +108,23 @@ } } finally { isLoading = false; + isResultsLoading = false; } } - $: displayedCompanies = companies.filter( - (c) => - c.name.toLowerCase().includes(searchQuery.toLowerCase()) || - c.cw_Identifier.toLowerCase().includes(searchQuery.toLowerCase()) || - c.cw_CompanyId.toString().includes(searchQuery), - ); + $: displayedCompanies = + searchQuery.trim().length > 0 + ? companies + : companies.filter( + (c) => + c.name.toLowerCase().includes(searchQuery.toLowerCase()) || + c.cw_Identifier.toLowerCase().includes(searchQuery.toLowerCase()) || + c.cw_CompanyId.toString().includes(searchQuery), + ); function goToPage(page: number) { if (page >= 1 && page <= totalPages) { - loadCompanies(page); + loadCompanies(page, searchQuery.trim() || undefined); } } @@ -115,11 +133,23 @@ } function retryLoad() { - loadCompanies(currentPage); + loadCompanies(currentPage, searchQuery.trim() || undefined); } // Load companies on component mount loadCompanies(); + + function onSearchInput() { + // Show inline spinner immediately while the user is typing. + isResultsLoading = true; + + if (searchTimeout) clearTimeout(searchTimeout); + searchTimeout = setTimeout(() => { + // Pass an explicit `search` arg (may be empty string) so loadCompanies + // treats this as a results-only refresh instead of a full-page reload. + loadCompanies(1, searchQuery.trim()); + }, 500); + } @@ -158,6 +188,7 @@ type="text" placeholder="Search companies by name, ID, or identifier..." bind:value={searchQuery} + on:input={onSearchInput} class="search-bar" /> {#if searchQuery} @@ -169,20 +200,37 @@ {#if displayedCompanies.length > 0}
- {#each displayedCompanies as comp (comp.id)} -
-

{comp.name}

-
-
CW Company ID
-
{comp.cw_CompanyId}
-
CW Identifier
-
{comp.cw_Identifier}
-
Created
-
{new Date(comp.createdAt).toLocaleDateString()}
-
- View Details -
- {/each} + {#if isResultsLoading} +
+ +
+ {:else} + {#each displayedCompanies as comp (comp.id)} +
goto(`/companies/${comp.id}`)} + on:keydown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + goto(`/companies/${comp.id}`); + } + }} + > +

{comp.name}

+
+
CW Company ID
+
{comp.cw_CompanyId}
+
CW Identifier
+
{comp.cw_Identifier}
+
Created
+
{new Date(comp.createdAt).toLocaleDateString()}
+
+ View Details +
+ {/each} + {/if}
{:else}
@@ -190,7 +238,7 @@
{/if} - {#if totalPages > 1 && !searchQuery} + {#if totalPages > 1 && !isResultsLoading}
+ + + +
+ {#if error} + + {:else} +
+

API Response

+
+ Show company JSON +
{JSON.stringify(data.company, null, 2)}
+
+
+
+

Configurations

+ {#if data.configurationsError} + + {:else if data.configurations} +
+ Show configurations JSON +
{JSON.stringify(data.configurations, null, 2)}
+
+ {:else} +

No configurations available for this company.

+ {/if} +
+ {/if} +
+ + diff --git a/src/routes/company/+page.svelte b/src/routes/company/+page.svelte index e69de29..153558f 100644 --- a/src/routes/company/+page.svelte +++ b/src/routes/company/+page.svelte @@ -0,0 +1,12 @@ + + + + + Redirecting... + + +

Redirecting to /companies

diff --git a/src/routes/company/[id]/+page.server.ts b/src/routes/company/[id]/+page.server.ts index 8498ae8..0f63ed9 100644 --- a/src/routes/company/[id]/+page.server.ts +++ b/src/routes/company/[id]/+page.server.ts @@ -16,8 +16,25 @@ export const load: PageServerLoad = async ({ params, parent }) => { throw error(404, `Company with ID ${params.id} not found`); } + // attempt to load configurations but don't fail the whole page if it errors + let configurations = null; + let configurationsError = null; + try { + configurations = await company.fetchConfigurations( + session.accessToken, + params.id, + ); + } catch (cfgErr) { + console.error("Failed to fetch configurations:", cfgErr); + configurationsError = String( + cfgErr instanceof Error ? cfgErr.message : cfgErr, + ); + } + return { company: companyData, + configurations, + configurationsError, }; } catch (err) { console.error("Failed to fetch company:", err); diff --git a/src/routes/company/[id]/+page.svelte b/src/routes/company/[id]/+page.svelte index 3152d20..14fc7ba 100644 --- a/src/routes/company/[id]/+page.svelte +++ b/src/routes/company/[id]/+page.svelte @@ -1,119 +1,25 @@ - Company Detail — App + + Redirecting... -
-

Company Detail

- -
- -
- {#if error} - - {:else} -
-

API Response

-
{JSON.stringify(data.company, null, 2)}
-
- {/if} -
- - +

Redirecting to /companies

diff --git a/src/routes/login/+page.svelte b/src/routes/login/+page.svelte index 99da6e9..d66b134 100644 --- a/src/routes/login/+page.svelte +++ b/src/routes/login/+page.svelte @@ -35,7 +35,7 @@
-
+