big boi refactor to customer inof from passenger info
This commit is contained in:
@@ -1 +0,0 @@
|
||||
export * from "@pkg/logic/domains/account/data/entities";
|
||||
@@ -1,137 +0,0 @@
|
||||
import { and, eq, inArray, notInArray, type Database } from "@pkg/db";
|
||||
import { inbox } from "@pkg/db/schema";
|
||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||
import { inboxModel, type InboxModel } from "./entities";
|
||||
import { getError, Logger } from "@pkg/logger";
|
||||
|
||||
export class AccountInboxRepository {
|
||||
db: Database;
|
||||
|
||||
constructor(db: Database) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async getAccountInbox(
|
||||
accountId: number,
|
||||
ignoreIds: number[] = [],
|
||||
): Promise<Result<InboxModel[]>> {
|
||||
let condition: any = eq(inbox.emailAccountId, accountId);
|
||||
if (ignoreIds.length > 0) {
|
||||
condition = and(condition, notInArray(inbox.id, ignoreIds));
|
||||
}
|
||||
|
||||
const queryRes = await this.db.query.inbox.findMany({
|
||||
where: condition,
|
||||
});
|
||||
|
||||
const out = [] as InboxModel[];
|
||||
|
||||
for (const row of queryRes) {
|
||||
const sParsed = inboxModel.safeParse(row);
|
||||
if (sParsed.success) {
|
||||
out.push(sParsed.data);
|
||||
continue;
|
||||
}
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message:
|
||||
"An error occured while fetching inbox for account",
|
||||
userHint: "Please try again later, or contact us",
|
||||
detail: "An error occured while fetching inbox for account",
|
||||
actionable: false,
|
||||
},
|
||||
sParsed.error.errors,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
return { data: out };
|
||||
}
|
||||
|
||||
// checks which of these are already present in the db
|
||||
async findEmailsAlreadyPresent(
|
||||
emailIds: string[],
|
||||
): Promise<Result<string[]>> {
|
||||
const found = await this.db.query.inbox.findMany({
|
||||
where: inArray(inbox.emailId, emailIds),
|
||||
columns: { emailId: true },
|
||||
});
|
||||
return { data: found.map((e) => e.emailId) };
|
||||
}
|
||||
|
||||
async insertMessages(
|
||||
accountId: number,
|
||||
messages: InboxModel[],
|
||||
): Promise<Result<number[]>> {
|
||||
try {
|
||||
const alreadyPresent = await this.findEmailsAlreadyPresent(
|
||||
messages.map((e) => e.emailId),
|
||||
);
|
||||
if (!alreadyPresent.data) {
|
||||
return { error: alreadyPresent.error };
|
||||
}
|
||||
const payload = messages
|
||||
.filter((e) => !alreadyPresent.data?.includes(e.emailId))
|
||||
.map((m) => {
|
||||
return {
|
||||
emailId: m.emailId,
|
||||
|
||||
from: m.from,
|
||||
to: m.to,
|
||||
cc: m.cc,
|
||||
subject: m.subject,
|
||||
body: m.body,
|
||||
attachments: m.attachments,
|
||||
|
||||
emailAccountId: accountId,
|
||||
|
||||
dated: new Date(m.dated),
|
||||
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
});
|
||||
if (payload.length === 0) {
|
||||
Logger.info(
|
||||
`No msgs to insert for acc:${accountId} (${alreadyPresent.data.length} already present)`,
|
||||
);
|
||||
return { data: [] };
|
||||
}
|
||||
|
||||
Logger.info(
|
||||
`Inserting ${messages.length} msgs for acc:${accountId} (${alreadyPresent.data.length} already present)`,
|
||||
);
|
||||
const out = await this.db
|
||||
.insert(inbox)
|
||||
.values(payload)
|
||||
.returning({ id: inbox.id })
|
||||
.execute();
|
||||
|
||||
return { data: out.map((e) => e.id) };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message:
|
||||
"An error occured while inserting messages for account",
|
||||
userHint: "Please try again later, or contact us",
|
||||
detail: "An error occured while inserting messages for account",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getInboxMessagesCount(accountId: number): Promise<Result<number>> {
|
||||
const res = await this.db.query.inbox.findMany({
|
||||
where: eq(inbox.emailAccountId, accountId),
|
||||
columns: { id: true },
|
||||
});
|
||||
return { data: res ? res.length : 0 };
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
import { eq, type Database } from "@pkg/db";
|
||||
import {
|
||||
emailAccountFullModel,
|
||||
emailAccountModel,
|
||||
type EmailAccount,
|
||||
type EmailAccountFull,
|
||||
} from "./entities";
|
||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||
import { Logger } from "@pkg/logger";
|
||||
import { getError } from "@pkg/logger";
|
||||
import { emailAccount } from "@pkg/db/schema";
|
||||
|
||||
export class AccountsRepository {
|
||||
db: Database;
|
||||
constructor(db: Database) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async listAllEmailAccounts(): Promise<Result<EmailAccount[]>> {
|
||||
try {
|
||||
const out = await this.db.query.emailAccount.findMany();
|
||||
Logger.info(out);
|
||||
const parsed = [] as EmailAccount[];
|
||||
for (const each of out) {
|
||||
const pRes = emailAccountModel.safeParse(each);
|
||||
if (pRes.error) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message:
|
||||
"An error occured while listing email accounts",
|
||||
userHint: "Please try again later",
|
||||
actionable: false,
|
||||
detail: "An error occured while listing email accounts",
|
||||
},
|
||||
pRes.error.errors,
|
||||
),
|
||||
};
|
||||
}
|
||||
parsed.push(pRes.data);
|
||||
}
|
||||
return { data: parsed };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "An error occured while listing email accounts",
|
||||
userHint: "Please try again later",
|
||||
actionable: false,
|
||||
detail: "An error occured while listing email accounts",
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getEmailAccount(id: number): Promise<Result<EmailAccount>> {
|
||||
try {
|
||||
const out = await this.db.query.emailAccount.findFirst({
|
||||
where: eq(emailAccount.id, id),
|
||||
});
|
||||
if (!out) {
|
||||
Logger.error("Failed to get email account");
|
||||
Logger.debug(out);
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to get email account",
|
||||
userHint: "Please try again",
|
||||
detail: "Failed to get email account",
|
||||
}),
|
||||
};
|
||||
}
|
||||
const pRes = emailAccountModel.safeParse(out);
|
||||
if (pRes.error) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to get email account",
|
||||
userHint: "Please try again",
|
||||
detail: "Failed to get email account",
|
||||
},
|
||||
pRes.error,
|
||||
),
|
||||
};
|
||||
}
|
||||
return { data: pRes.data };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "An error occured while getting email account",
|
||||
userHint: "Please try again later",
|
||||
actionable: false,
|
||||
detail: "An error occured while getting email account",
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getEmailAccountWithPassword(
|
||||
id: number,
|
||||
): Promise<Result<EmailAccountFull>> {
|
||||
try {
|
||||
const out = await this.db.query.emailAccount.findFirst({
|
||||
where: eq(emailAccount.id, id),
|
||||
});
|
||||
if (!out) {
|
||||
Logger.error("Failed to get email account");
|
||||
Logger.debug(out);
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to get email account",
|
||||
userHint: "Please try again",
|
||||
detail: "Failed to get email account",
|
||||
}),
|
||||
};
|
||||
}
|
||||
const pRes = emailAccountFullModel.safeParse(out);
|
||||
if (pRes.error) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to get email account",
|
||||
userHint: "Please try again",
|
||||
detail: "Failed to get email account",
|
||||
},
|
||||
pRes.error,
|
||||
),
|
||||
};
|
||||
}
|
||||
return { data: pRes.data };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "An error occured while getting email account",
|
||||
userHint: "Please try again later",
|
||||
actionable: false,
|
||||
detail: "An error occured while getting email account",
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// selector is either an email or an id
|
||||
async updateLastActiveCheckAt(selector: number | string, ts: string) {
|
||||
Logger.info(`Updating last active check at for ${selector}`);
|
||||
try {
|
||||
await this.db
|
||||
.update(emailAccount)
|
||||
.set({ lastActiveCheckAt: new Date(ts) })
|
||||
.where(
|
||||
typeof selector === "string"
|
||||
? eq(emailAccount.email, selector)
|
||||
: eq(emailAccount.id, selector),
|
||||
)
|
||||
.execute();
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
message: "Failed to update last active check at",
|
||||
code: ERROR_CODES.DATABASE_ERROR,
|
||||
detail: "A database error occured while updating last active check at",
|
||||
userHint: "Please try again later",
|
||||
actionable: false,
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import type { AccountInboxRepository } from "../data/inbox.repository";
|
||||
import type { AccountsRepository } from "../data/repository";
|
||||
import { type InboxModel } from "../data/entities";
|
||||
import { type Result } from "@pkg/result";
|
||||
|
||||
export class AccountsController {
|
||||
repo: AccountsRepository;
|
||||
inboxRepo: AccountInboxRepository;
|
||||
|
||||
constructor(repo: AccountsRepository, inboxRepo: AccountInboxRepository) {
|
||||
this.repo = repo;
|
||||
this.inboxRepo = inboxRepo;
|
||||
}
|
||||
|
||||
async listAllEmailAccounts() {
|
||||
return this.repo.listAllEmailAccounts();
|
||||
}
|
||||
|
||||
async getEmailAccount(id: number) {
|
||||
return this.repo.getEmailAccount(id);
|
||||
}
|
||||
|
||||
async getAccountInbox(accountId: number, ignoreIds: number[]) {
|
||||
return this.inboxRepo.getAccountInbox(accountId);
|
||||
}
|
||||
|
||||
async checkAccountActiveStatus(
|
||||
id: number,
|
||||
): Promise<Result<{ ok: boolean; ts: string }>> {
|
||||
return { data: { ok: false, ts: new Date().toISOString() } };
|
||||
}
|
||||
|
||||
async refreshInbox(
|
||||
accountId: number,
|
||||
params?: any,
|
||||
): Promise<Result<InboxModel[]>> {
|
||||
return { data: [] };
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { createTRPCRouter, protectedProcedure } from "$lib/trpc/t";
|
||||
import { db } from "@pkg/db";
|
||||
import { AccountsRepository } from "../data/repository";
|
||||
import { AccountsController } from "./controller";
|
||||
import { AccountInboxRepository } from "../data/inbox.repository";
|
||||
import { z } from "zod";
|
||||
|
||||
function getEAC() {
|
||||
return new AccountsController(
|
||||
new AccountsRepository(db),
|
||||
new AccountInboxRepository(db),
|
||||
);
|
||||
}
|
||||
|
||||
export const emailAccountsRouter = createTRPCRouter({
|
||||
listAllEmailAccounts: protectedProcedure.query(async ({ ctx }) => {
|
||||
return getEAC().listAllEmailAccounts();
|
||||
}),
|
||||
|
||||
getEmailAccount: protectedProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
return getEAC().getEmailAccount(input.id);
|
||||
}),
|
||||
|
||||
checkAccountActiveStatus: protectedProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
return getEAC().checkAccountActiveStatus(input.id);
|
||||
}),
|
||||
|
||||
getAccountInbox: protectedProcedure
|
||||
.input(z.object({ id: z.number(), ignoreIds: z.array(z.number()) }))
|
||||
.query(async ({ input }) => {
|
||||
return getEAC().getAccountInbox(input.id, input.ignoreIds);
|
||||
}),
|
||||
|
||||
refreshInbox: protectedProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
return getEAC().refreshInbox(input.id);
|
||||
}),
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
export function parseFrom(from: string) {
|
||||
const match = from.match(/"(.*?)"\s*<(.+?)>/);
|
||||
if (match) {
|
||||
return { name: match[1], email: match[2] };
|
||||
} else {
|
||||
// Handle cases where the name might be missing
|
||||
const emailOnly = from.match(/<(.+?)>/);
|
||||
return { name: null, email: emailOnly ? emailOnly[1] : from };
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
<script lang="ts">
|
||||
import BillListIcon from "~icons/solar/bill-list-linear";
|
||||
import Icon from "$lib/components/atoms/icon.svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import { accountsVM } from "./accounts.vm.svelte";
|
||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||
import Badge from "$lib/components/ui/badge/badge.svelte";
|
||||
import ButtonLoadableText from "$lib/components/atoms/button-loadable-text.svelte";
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open={accountsVM.showAccountsList}>
|
||||
<Button
|
||||
size="iconSm"
|
||||
variant="outlineWhite"
|
||||
onclick={() => (accountsVM.showAccountsList = true)}
|
||||
>
|
||||
<Icon icon={BillListIcon} cls="h-6 w-6" />
|
||||
</Button>
|
||||
|
||||
<Dialog.Content class="max-w-lg md:max-w-2xl">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Agents' Email Accounts</Dialog.Title>
|
||||
</Dialog.Header>
|
||||
<div class="h-[70vh] w-full overflow-y-auto">
|
||||
{#each accountsVM.accounts as acc}
|
||||
{@const isCheckingFor =
|
||||
accountsVM.checkingForId === acc.id &&
|
||||
accountsVM.checkingAccountActiveStatus}
|
||||
<div
|
||||
class="flex w-full items-start justify-between gap-2 rounded-md border border-gray-200 bg-white p-2 shadow-md md:p-4"
|
||||
>
|
||||
<div class="flex h-full flex-col justify-between gap-3">
|
||||
<p class="text-sm font-medium md:text-lg">{acc.email}</p>
|
||||
<div class="h-full justify-end">
|
||||
{#if !isCheckingFor}
|
||||
<p class="text-xs text-gray-600">
|
||||
Last Checked For Activity :
|
||||
{acc.lastActiveCheckAt
|
||||
? new Date(
|
||||
acc.lastActiveCheckAt,
|
||||
).toLocaleString()
|
||||
: "Never"}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-xs text-gray-300">checking...</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex h-full flex-col items-end justify-between gap-2"
|
||||
>
|
||||
<div class="w-max">
|
||||
<Badge variant="outline">
|
||||
{acc.used ? "Not used" : "Used"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onclick={() =>
|
||||
accountsVM.checkAccountsActiveStatus(acc.id)}
|
||||
size="sm"
|
||||
variant="outlineWhite"
|
||||
disabled={isCheckingFor}
|
||||
>
|
||||
<ButtonLoadableText
|
||||
loading={isCheckingFor}
|
||||
text={"Check for activity"}
|
||||
loadingText={"Checking"}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
@@ -1,130 +0,0 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getFilteredRowModel,
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
type ColumnFiltersState,
|
||||
} from "@tanstack/table-core";
|
||||
import {
|
||||
createSvelteTable,
|
||||
renderComponent,
|
||||
} from "$lib/components/ui/data-table";
|
||||
import DataTable from "$lib/components/molecules/data-table/data-table.svelte";
|
||||
import DataTableActions from "$lib/components/molecules/data-table/data-table-actions.svelte";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { adminSiteNavMap } from "$lib/core/constants";
|
||||
import { useDebounce } from "runed";
|
||||
import type { EmailAccount } from "../data/entities";
|
||||
|
||||
let { data }: { data: EmailAccount[] } = $props();
|
||||
|
||||
// Define columns
|
||||
const columns: ColumnDef<any>[] = [
|
||||
{
|
||||
header: "#",
|
||||
accessorKey: "id",
|
||||
cell: ({ row }) => {
|
||||
return Number(row.id) + 1;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Email",
|
||||
id: "email",
|
||||
cell: ({ row }) => {
|
||||
return row.original.email;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Action",
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
return renderComponent(DataTableActions, {
|
||||
actions: [
|
||||
{
|
||||
title: "View Details",
|
||||
action: () => {
|
||||
goto(
|
||||
`${adminSiteNavMap.endusers}/${row.original.id}`,
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let pagination = $state<PaginationState>({ pageIndex: 0, pageSize: 10 });
|
||||
let columnFilters = $state<ColumnFiltersState>([]);
|
||||
|
||||
const table = createSvelteTable({
|
||||
get data() {
|
||||
return data;
|
||||
},
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onPaginationChange: (updater) => {
|
||||
if (typeof updater === "function") {
|
||||
pagination = updater(pagination);
|
||||
} else {
|
||||
pagination = updater;
|
||||
}
|
||||
},
|
||||
onColumnFiltersChange: (updater) => {
|
||||
if (typeof updater === "function") {
|
||||
columnFilters = updater(columnFilters);
|
||||
} else {
|
||||
columnFilters = updater;
|
||||
}
|
||||
},
|
||||
state: {
|
||||
get pagination() {
|
||||
return pagination;
|
||||
},
|
||||
get columnFilters() {
|
||||
return columnFilters;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
let debounceDuration = $state(1000);
|
||||
const debouncedSearch = useDebounce(
|
||||
() => {
|
||||
console.log("Debounced search - NYI");
|
||||
},
|
||||
() => debounceDuration,
|
||||
);
|
||||
|
||||
let pageCount = $derived(2);
|
||||
</script>
|
||||
|
||||
{#if data.length > 0}
|
||||
<DataTable
|
||||
{table}
|
||||
onNextPageClick={() => {
|
||||
table.nextPage();
|
||||
}}
|
||||
onPreviousPageClick={() => {
|
||||
table.previousPage();
|
||||
}}
|
||||
currPage={pagination.pageIndex + 1}
|
||||
totalPages={pageCount}
|
||||
filterFieldDisabled={false}
|
||||
query={passengerInfoVM.query}
|
||||
hasData={data.length > 0}
|
||||
onQueryChange={(q) => {
|
||||
passengerInfoVM.query = q;
|
||||
debouncedSearch();
|
||||
}}
|
||||
filterFieldPlaceholder="Search users..."
|
||||
/>
|
||||
{:else}
|
||||
<div class="grid h-full place-items-center p-4 py-12 md:p-8 md:py-32">
|
||||
<Title size="h5">No Orders found</Title>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { trpcApiStore } from "$lib/stores/api";
|
||||
import { get } from "svelte/store";
|
||||
import { type InboxModel, type EmailAccount } from "../data/entities";
|
||||
import { toast } from "svelte-sonner";
|
||||
|
||||
export class AccountsViewModel {
|
||||
accounts = $state([] as EmailAccount[]);
|
||||
|
||||
chosenAccountId = $state<number | undefined>(undefined);
|
||||
|
||||
accountInbox = $state<InboxModel[]>([]);
|
||||
fetchingInbox = $state(false);
|
||||
chosenMessageId = $state<number | undefined>(undefined);
|
||||
|
||||
showAccountsList = $state(false);
|
||||
checkingAccountActiveStatus = $state(false);
|
||||
checkingForId = $state<number | undefined>(undefined);
|
||||
|
||||
loading = $state(false);
|
||||
query = $state("");
|
||||
|
||||
async fetchAccounts() {
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
return toast("Api not initialized", {
|
||||
description: "Try refreshing the page",
|
||||
});
|
||||
}
|
||||
this.loading = true;
|
||||
const result = await api.emailAccounts.listAllEmailAccounts.query();
|
||||
this.loading = false;
|
||||
if (result.error) {
|
||||
return toast.error(result.error.message, {
|
||||
description: result.error.userHint,
|
||||
});
|
||||
}
|
||||
if (!!result.data) {
|
||||
this.accounts = result.data;
|
||||
}
|
||||
}
|
||||
|
||||
async checkAccountsActiveStatus(id: number) {
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
return toast("Api not initialized", {
|
||||
description: "Try refreshing the page",
|
||||
});
|
||||
}
|
||||
this.checkingAccountActiveStatus = true;
|
||||
this.checkingForId = id;
|
||||
const result = await api.emailAccounts.checkAccountActiveStatus.query({
|
||||
id,
|
||||
});
|
||||
this.checkingAccountActiveStatus = false;
|
||||
this.checkingForId = undefined;
|
||||
console.log(result);
|
||||
if (result.error) {
|
||||
return toast.error(result.error.message, {
|
||||
description: result.error.userHint,
|
||||
});
|
||||
}
|
||||
if (!!result.data && !!result.data.ok) {
|
||||
for (let i = 0; i < this.accounts.length; i++) {
|
||||
if (this.accounts[i].id === id) {
|
||||
this.accounts[i].lastActiveCheckAt = result.data.ts;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fetchInbox() {
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
return toast("Api not initialized", {
|
||||
description: "Try refreshing the page",
|
||||
});
|
||||
}
|
||||
this.loading = true;
|
||||
if (!this.chosenAccountId) {
|
||||
return toast.error("No account selected", {
|
||||
description: "Please select an account to perform action",
|
||||
});
|
||||
}
|
||||
const result = await api.emailAccounts.getAccountInbox.query({
|
||||
id: this.chosenAccountId,
|
||||
ignoreIds: this.accountInbox.map((i) => i.id),
|
||||
});
|
||||
this.loading = false;
|
||||
console.log(result);
|
||||
if (result.error) {
|
||||
return toast.error(result.error.message, {
|
||||
description: result.error.userHint,
|
||||
});
|
||||
}
|
||||
if (!!result.data) {
|
||||
this.upsertMessages(result.data, true);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshInbox() {
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
return toast("Api not initialized", {
|
||||
description: "Try refreshing the page",
|
||||
});
|
||||
}
|
||||
if (!this.chosenAccountId) {
|
||||
return toast.error("No account selected", {
|
||||
description: "Please select an account to perform action",
|
||||
});
|
||||
}
|
||||
this.fetchingInbox = true;
|
||||
const result = await api.emailAccounts.refreshInbox.mutate({
|
||||
id: this.chosenAccountId,
|
||||
});
|
||||
this.fetchingInbox = false;
|
||||
if (result.error) {
|
||||
return toast.error(result.error.message, {
|
||||
description: result.error.userHint,
|
||||
});
|
||||
}
|
||||
if (result.data) {
|
||||
this.upsertMessages(result.data);
|
||||
}
|
||||
}
|
||||
|
||||
private upsertMessages(messages: InboxModel[], reset = false) {
|
||||
if (reset) {
|
||||
this.accountInbox = messages;
|
||||
return;
|
||||
}
|
||||
|
||||
for (const message of messages) {
|
||||
const existing = this.accountInbox.find((m) => m.id === message.id);
|
||||
if (existing) {
|
||||
Object.assign(existing, message);
|
||||
} else {
|
||||
this.accountInbox.push(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const accountsVM = new AccountsViewModel();
|
||||
@@ -1,160 +0,0 @@
|
||||
<script lang="ts">
|
||||
import * as Avatar from "$lib/components/ui/avatar/index.js";
|
||||
import type { InboxModel } from "../data/entities";
|
||||
import Container from "$lib/components/atoms/container.svelte";
|
||||
import { cn } from "$lib/utils";
|
||||
import { CARD_STYLE } from "$lib/core/constants";
|
||||
import { parseFrom } from "../utils";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
|
||||
let { message }: { message?: InboxModel } = $props();
|
||||
|
||||
let parsedFrom = $derived(parseFrom(message?.from ?? ""));
|
||||
|
||||
function createEmailHTML(content: string) {
|
||||
return `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<base target="_blank">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self' 'unsafe-inline' data: https:; img-src * data: https:;">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
a {
|
||||
color: #0066cc;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
table {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
${content}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
||||
|
||||
function handleIframeLoad(iframe: HTMLIFrameElement) {
|
||||
try {
|
||||
const body = iframe.contentWindow?.document.body;
|
||||
if (body) {
|
||||
// Set iframe height to 100% instead of calculating it
|
||||
iframe.style.height = "100%";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to adjust iframe:", e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if message}
|
||||
<div class="flex h-full flex-col gap-4">
|
||||
<!-- Email Header Section -->
|
||||
<div
|
||||
class={cn("flex flex-col gap-4 rounded-lg bg-white p-4", CARD_STYLE)}
|
||||
>
|
||||
<div class="flex flex-col gap-2 sm:flex-row">
|
||||
<Avatar.Root class="h-10 w-10 rounded-full">
|
||||
<Avatar.Fallback class="rounded-full">
|
||||
{parsedFrom.name?.slice(0, 2).toUpperCase() ??
|
||||
parsedFrom.email?.slice(0, 2).toUpperCase() ??
|
||||
"U"}
|
||||
</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
|
||||
<div
|
||||
class="flex w-full flex-col items-start gap-2 md:flex-row md:justify-between"
|
||||
>
|
||||
<div class="flex flex-col gap-2 sm:p-1">
|
||||
<div class="flex flex-col">
|
||||
<Title size="h5" color="black" weight="medium">
|
||||
{parsedFrom.name}
|
||||
</Title>
|
||||
|
||||
<small class="text-gray-600">
|
||||
{parsedFrom.email}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<p>{message.subject}</p>
|
||||
|
||||
{#if message.to}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">To:</span>
|
||||
<span class="text-gray-600">{message.to}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if message.cc}
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium">CC:</span>
|
||||
<span class="text-gray-600">{message.cc}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col items-end gap-2">
|
||||
<small class="text-gray-600">
|
||||
{new Date(message.dated).toLocaleString()}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if message.attachments && message.attachments.length > 0}
|
||||
<div class="border-t pt-4">
|
||||
<h3 class="mb-2 font-medium">Attachments:</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each message.attachments as attachment}
|
||||
<div
|
||||
class="flex items-center gap-2 rounded-md border bg-gray-50 px-3 py-2"
|
||||
>
|
||||
<span>{attachment.filename}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Email Body Section with iframe -->
|
||||
<div class={cn("flex-1 rounded-lg bg-white", CARD_STYLE)}>
|
||||
<iframe
|
||||
title="Email Content"
|
||||
srcdoc={createEmailHTML(message.body)}
|
||||
class="w-full border-none"
|
||||
sandbox="allow-same-origin allow-popups allow-popups-to-escape-sandbox"
|
||||
onload={(e) => {
|
||||
// @ts-ignore
|
||||
handleIframeLoad(e.currentTarget);
|
||||
}}
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex h-full flex-col gap-4">
|
||||
<Container containerClass="flex flex-col gap-4 p-4 bg-white rounded-lg">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h2 class="text-xl font-semibold">No message selected</h2>
|
||||
</div>
|
||||
</Container>
|
||||
</div>
|
||||
@@ -1,39 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { accountsVM } from "$lib/domains/account/view/accounts.vm.svelte";
|
||||
import { parseFrom } from "$lib/domains/account/utils";
|
||||
import { cn } from "$lib/utils";
|
||||
import { TRANSITION_ALL } from "$lib/core/constants";
|
||||
</script>
|
||||
|
||||
{#each accountsVM.accountInbox.toReversed() as each, idx}
|
||||
{@const parsed = parseFrom(each.from)}
|
||||
<button
|
||||
class={cn(
|
||||
"hover:bg-gray-10 flex w-full cursor-pointer flex-col items-start gap-2 rounded-lg border-2 border-gray-200 bg-white p-2 px-3 text-start hover:drop-shadow-md",
|
||||
TRANSITION_ALL,
|
||||
)}
|
||||
onclick={() => {
|
||||
accountsVM.chosenMessageId = each.id;
|
||||
}}
|
||||
>
|
||||
<div class="flex w-full flex-col justify-between">
|
||||
{#if parsed.name}
|
||||
<span class="font-medium">
|
||||
{parsed.name}
|
||||
</span>
|
||||
{/if}
|
||||
{#if parsed.email}
|
||||
<span class="text-sm text-gray-400">
|
||||
{parsed.email}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
<small class="text-sm">
|
||||
{each.subject.slice(0, 50)}
|
||||
{each.subject.length > 50 ? "..." : ""}
|
||||
</small>
|
||||
<small class="w-full text-end text-[0.7rem]">
|
||||
{new Date(each.dated).toLocaleString()}
|
||||
</small>
|
||||
</button>
|
||||
{/each}
|
||||
1
apps/admin/src/lib/domains/customerinfo/data.ts
Normal file
1
apps/admin/src/lib/domains/customerinfo/data.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "@pkg/logic/domains/customerinfo/data";
|
||||
1
apps/admin/src/lib/domains/customerinfo/repository.ts
Normal file
1
apps/admin/src/lib/domains/customerinfo/repository.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "@pkg/logic/domains/customerinfo/repository";
|
||||
47
apps/admin/src/lib/domains/customerinfo/router.ts
Normal file
47
apps/admin/src/lib/domains/customerinfo/router.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { protectedProcedure } from "$lib/server/trpc/t";
|
||||
import { createTRPCRouter } from "$lib/trpc/t";
|
||||
import { z } from "zod";
|
||||
import { createCustomerInfoPayload, updateCustomerInfoPayload } from "./data";
|
||||
import { getCustomerInfoUseCases } from "./usecases";
|
||||
|
||||
export const customerInfoRouter = createTRPCRouter({
|
||||
getAllCustomerInfo: protectedProcedure.query(async ({}) => {
|
||||
const controller = getCustomerInfoUseCases();
|
||||
return controller.getAllCustomerInfo();
|
||||
}),
|
||||
|
||||
getCustomerInfoById: protectedProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const controller = getCustomerInfoUseCases();
|
||||
return controller.getCustomerInfoById(input.id);
|
||||
}),
|
||||
|
||||
getCustomerInfoByOrderId: protectedProcedure
|
||||
.input(z.object({ orderId: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const controller = getCustomerInfoUseCases();
|
||||
return controller.getCustomerInfoByOrderId(input.orderId);
|
||||
}),
|
||||
|
||||
createCustomerInfo: protectedProcedure
|
||||
.input(createCustomerInfoPayload)
|
||||
.mutation(async ({ input }) => {
|
||||
const controller = getCustomerInfoUseCases();
|
||||
return controller.createCustomerInfo(input);
|
||||
}),
|
||||
|
||||
updateCustomerInfo: protectedProcedure
|
||||
.input(updateCustomerInfoPayload)
|
||||
.mutation(async ({ input }) => {
|
||||
const controller = getCustomerInfoUseCases();
|
||||
return controller.updateCustomerInfo(input);
|
||||
}),
|
||||
|
||||
deleteCustomerInfo: protectedProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const controller = getCustomerInfoUseCases();
|
||||
return controller.deleteCustomerInfo(input.id);
|
||||
}),
|
||||
});
|
||||
1
apps/admin/src/lib/domains/customerinfo/usecases.ts
Normal file
1
apps/admin/src/lib/domains/customerinfo/usecases.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "@pkg/logic/domains/customerinfo/usecases";
|
||||
@@ -0,0 +1,57 @@
|
||||
<script lang="ts">
|
||||
import UserIcon from "~icons/solar/user-broken";
|
||||
import type { CustomerInfoModel } from "../data";
|
||||
import InfoCard from "./info-card.svelte";
|
||||
|
||||
let {
|
||||
customerInfo,
|
||||
}: {
|
||||
customerInfo: CustomerInfoModel;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<InfoCard icon={UserIcon} title="Customer Information">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">Full Name</span>
|
||||
<p>
|
||||
{customerInfo.firstName}
|
||||
{#if customerInfo.middleName}
|
||||
{customerInfo.middleName}
|
||||
{/if}
|
||||
{customerInfo.lastName}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">Email</span>
|
||||
<p>{customerInfo.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">Phone Number</span>
|
||||
<p>{customerInfo.phoneCountryCode} {customerInfo.phoneNumber}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">City</span>
|
||||
<p>{customerInfo.city}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">State</span>
|
||||
<p>{customerInfo.state}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">Country</span>
|
||||
<p>{customerInfo.country}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">Zip Code</span>
|
||||
<p>{customerInfo.zipCode}</p>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<span class="text-xs text-gray-500">Address</span>
|
||||
<p>{customerInfo.address}</p>
|
||||
{#if customerInfo.address2}
|
||||
<p class="text-sm text-gray-600">{customerInfo.address2}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</InfoCard>
|
||||
@@ -1,29 +1,29 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
getFilteredRowModel,
|
||||
type ColumnDef,
|
||||
type PaginationState,
|
||||
type ColumnFiltersState,
|
||||
} from "@tanstack/table-core";
|
||||
import { goto } from "$app/navigation";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import DataTableActions from "$lib/components/molecules/data-table/data-table-actions.svelte";
|
||||
import DataTable from "$lib/components/molecules/data-table/data-table.svelte";
|
||||
import {
|
||||
createSvelteTable,
|
||||
renderComponent,
|
||||
} from "$lib/components/ui/data-table";
|
||||
import DataTable from "$lib/components/molecules/data-table/data-table.svelte";
|
||||
import DataTableActions from "$lib/components/molecules/data-table/data-table-actions.svelte";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import { goto } from "$app/navigation";
|
||||
import { adminSiteNavMap } from "$lib/core/constants";
|
||||
import {
|
||||
getCoreRowModel,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
type ColumnDef,
|
||||
type ColumnFiltersState,
|
||||
type PaginationState,
|
||||
} from "@tanstack/table-core";
|
||||
import { useDebounce } from "runed";
|
||||
import type { PassengerInfo } from "../data/entities";
|
||||
import { passengerInfoVM } from "./passengerinfo.vm.svelte";
|
||||
import type { CustomerInfoModel } from "../data";
|
||||
import { customerInfoVM } from "./customerinfo.vm.svelte";
|
||||
|
||||
let { data }: { data: PassengerInfo[] } = $props();
|
||||
let { data }: { data: CustomerInfoModel[] } = $props();
|
||||
|
||||
// Define columns
|
||||
const columns: ColumnDef<any>[] = [
|
||||
const columns: ColumnDef<CustomerInfoModel>[] = [
|
||||
{
|
||||
header: "#",
|
||||
accessorKey: "id",
|
||||
@@ -33,25 +33,30 @@
|
||||
},
|
||||
{
|
||||
header: "Email",
|
||||
id: "email",
|
||||
cell: ({ row }) => {
|
||||
return row.original.passengerPii.email;
|
||||
},
|
||||
accessorKey: "email",
|
||||
},
|
||||
|
||||
{
|
||||
header: "Phone No",
|
||||
id: "phone",
|
||||
cell: ({ row }) => {
|
||||
return row.original.passengerPii.phoneNumber;
|
||||
return `${row.original.phoneCountryCode} ${row.original.phoneNumber}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Name",
|
||||
id: "name",
|
||||
cell: ({ row }) => {
|
||||
const pii = (row.original as PassengerInfo).passengerPii;
|
||||
return `${pii.firstName} ${pii.lastName}`;
|
||||
const middleName = row.original.middleName
|
||||
? ` ${row.original.middleName}`
|
||||
: "";
|
||||
return `${row.original.firstName}${middleName} ${row.original.lastName}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Location",
|
||||
id: "location",
|
||||
cell: ({ row }) => {
|
||||
return `${row.original.city}, ${row.original.state}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -132,13 +137,13 @@
|
||||
currPage={pagination.pageIndex + 1}
|
||||
totalPages={pageCount}
|
||||
filterFieldDisabled={false}
|
||||
query={passengerInfoVM.query}
|
||||
query={customerInfoVM.query}
|
||||
hasData={data.length > 0}
|
||||
onQueryChange={(q) => {
|
||||
passengerInfoVM.query = q;
|
||||
customerInfoVM.query = q;
|
||||
debouncedSearch();
|
||||
}}
|
||||
filterFieldPlaceholder="Search users..."
|
||||
filterFieldPlaceholder="Search customers..."
|
||||
/>
|
||||
{:else}
|
||||
<div class="grid h-full place-items-center p-4 py-12 md:p-8 md:py-32">
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { CustomerInfoModel } from "../data";
|
||||
|
||||
export class CustomerInfoViewModel {
|
||||
customerInfos = $state([] as CustomerInfoModel[]);
|
||||
|
||||
loading = $state(false);
|
||||
query = $state("");
|
||||
}
|
||||
|
||||
export const customerInfoVM = new CustomerInfoViewModel();
|
||||
@@ -1,8 +1,8 @@
|
||||
import { count, eq, type Database } from "@pkg/db";
|
||||
import { ERROR_CODES, type Result } from "$lib/core/data.types";
|
||||
import { fullOrderModel, type FullOrderModel } from "./entities";
|
||||
import { count, eq, type Database } from "@pkg/db";
|
||||
import { customerInfo, order } from "@pkg/db/schema";
|
||||
import { getError, Logger } from "@pkg/logger";
|
||||
import { order, passengerInfo } from "@pkg/db/schema";
|
||||
import { fullOrderModel, type FullOrderModel } from "./entities";
|
||||
|
||||
export class OrderRepository {
|
||||
private db: Database;
|
||||
@@ -32,7 +32,7 @@ export class OrderRepository {
|
||||
for (const each of res) {
|
||||
const parsed = fullOrderModel.safeParse({
|
||||
...each,
|
||||
passengerInfos: [],
|
||||
customerInfos: [],
|
||||
});
|
||||
if (!parsed.success) {
|
||||
Logger.error(JSON.stringify(parsed.error.errors, null, 2));
|
||||
@@ -67,13 +67,12 @@ export class OrderRepository {
|
||||
},
|
||||
});
|
||||
if (!out) return {};
|
||||
const relatedPassengerInfos = await this.db.query.passengerInfo.findMany({
|
||||
where: eq(passengerInfo.orderId, oid),
|
||||
with: { passengerPii: true },
|
||||
const relatedCustomerInfos = await this.db.query.customerInfo.findMany({
|
||||
where: eq(customerInfo.orderId, oid),
|
||||
});
|
||||
const parsed = fullOrderModel.safeParse({
|
||||
...out,
|
||||
passengerInfos: relatedPassengerInfos,
|
||||
customerInfo: relatedCustomerInfos,
|
||||
});
|
||||
if (!parsed.success) {
|
||||
return {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "@pkg/logic/domains/passengerinfo/data/entities";
|
||||
@@ -1,89 +0,0 @@
|
||||
import { eq, inArray, type Database } from "@pkg/db";
|
||||
import { passengerInfo } from "@pkg/db/schema";
|
||||
import { ERROR_CODES, type Result } from "@pkg/result";
|
||||
import type { PassengerInfo } from "./entities";
|
||||
import { getError, Logger } from "@pkg/logger";
|
||||
|
||||
export class PassengerInfoRepository {
|
||||
private db: Database;
|
||||
|
||||
constructor(db: Database) {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async listAllPassengerInfos(): Promise<Result<PassengerInfo[]>> {
|
||||
try {
|
||||
const out = await this.db.query.passengerInfo.findMany({
|
||||
with: {
|
||||
passengerPii: true,
|
||||
parentAgent: { columns: { username: true } },
|
||||
},
|
||||
});
|
||||
return { data: out as any as PassengerInfo[] };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "An error occured while listing passenger infos",
|
||||
userHint: "Please try again later",
|
||||
actionable: false,
|
||||
detail: "An error occured while listing passenger infos",
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getPassengerInfo(id: number): Promise<Result<PassengerInfo>> {
|
||||
try {
|
||||
const out = await this.db.query.passengerInfo.findFirst({
|
||||
where: eq(passengerInfo.id, id),
|
||||
with: {
|
||||
passengerPii: true,
|
||||
paymentDetails: true,
|
||||
flightTicketInfo: { columns: { id: true } },
|
||||
order: { columns: { id: true } },
|
||||
parentAgent: { columns: { username: true } },
|
||||
},
|
||||
});
|
||||
if (!out) {
|
||||
Logger.error("Failed to get passenger info");
|
||||
Logger.debug(out);
|
||||
return {
|
||||
error: getError({
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "Failed to get passenger info",
|
||||
userHint: "Please try again",
|
||||
detail: "Failed to get passenger info",
|
||||
}),
|
||||
};
|
||||
}
|
||||
return { data: out as any as PassengerInfo };
|
||||
} catch (e) {
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.INTERNAL_SERVER_ERROR,
|
||||
message: "An error occured while getting passenger info",
|
||||
userHint: "Please try again later",
|
||||
actionable: false,
|
||||
detail: "An error occured while getting passenger info",
|
||||
},
|
||||
e,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async deleteAll(ids: number[]): Promise<Result<number>> {
|
||||
Logger.info(`Deleting ${ids.length} passenger info`);
|
||||
const out = await this.db
|
||||
.delete(passengerInfo)
|
||||
.where(inArray(passengerInfo.id, ids));
|
||||
Logger.debug(out);
|
||||
Logger.info(`Deleted ${out.count} passenger info`);
|
||||
return { data: out.count };
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { Result } from "@pkg/result";
|
||||
import type { PassengerInfo } from "../data/entities";
|
||||
import type { PassengerInfoRepository } from "../data/repository";
|
||||
|
||||
export class PassengerInfoController {
|
||||
repo: PassengerInfoRepository;
|
||||
|
||||
constructor(repo: PassengerInfoRepository) {
|
||||
this.repo = repo;
|
||||
}
|
||||
|
||||
async listAllPassengerInfos() {
|
||||
return this.repo.listAllPassengerInfos();
|
||||
}
|
||||
|
||||
async getPassengerInfo(id: number): Promise<Result<PassengerInfo>> {
|
||||
return this.repo.getPassengerInfo(id);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { PassengerInfo } from "../data/entities";
|
||||
|
||||
export class PassengerInfoViewModel {
|
||||
passengerInfos = $state([] as PassengerInfo[]);
|
||||
|
||||
loading = $state(false);
|
||||
query = $state("");
|
||||
}
|
||||
|
||||
export const passengerInfoVM = new PassengerInfoViewModel();
|
||||
@@ -1,43 +0,0 @@
|
||||
<script lang="ts">
|
||||
import PInfoCard from "./pinfo-card.svelte";
|
||||
import CreditCardIcon from "~icons/solar/card-broken";
|
||||
|
||||
let {
|
||||
paymentDetails,
|
||||
showFullDetails = false,
|
||||
}: {
|
||||
paymentDetails: any;
|
||||
showFullDetails?: boolean;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
{#if paymentDetails}
|
||||
<PInfoCard icon={CreditCardIcon} title="Payment Information">
|
||||
<div class="grid grid-cols-1 gap-2 md:grid-cols-2">
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">Cardholder Name</span>
|
||||
<p>{paymentDetails.cardholderName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">Card Number</span>
|
||||
<p>
|
||||
{#if showFullDetails}
|
||||
{paymentDetails.cardNumber}
|
||||
{:else}
|
||||
•••• •••• •••• {paymentDetails.cardNumber.slice(-4)}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">Expiry</span>
|
||||
<p>{paymentDetails.expiry}</p>
|
||||
</div>
|
||||
{#if showFullDetails}
|
||||
<div>
|
||||
<span class="text-xs text-gray-500">CVV</span>
|
||||
<p>{paymentDetails.cvv}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</PInfoCard>
|
||||
{/if}
|
||||
@@ -117,7 +117,9 @@
|
||||
<div class="font-medium text-gray-900">
|
||||
{product.title}
|
||||
</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
<div
|
||||
class="max-w-48 overflow-hidden text-ellipsis text-sm text-gray-500"
|
||||
>
|
||||
{product.description}
|
||||
</div>
|
||||
</td>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { emailAccountsRouter } from "$lib/domains/account/domain/router";
|
||||
import { authRouter } from "$lib/domains/auth/domain/router";
|
||||
import { ckflowRouter } from "$lib/domains/ckflow/router";
|
||||
import { couponRouter } from "$lib/domains/coupon/router";
|
||||
import { customerInfoRouter } from "$lib/domains/customerinfo/router";
|
||||
import { orderRouter } from "$lib/domains/order/domain/router";
|
||||
import { productRouter } from "$lib/domains/product/router";
|
||||
import { userRouter } from "$lib/domains/user/domain/router";
|
||||
@@ -11,10 +11,10 @@ export const router = createTRPCRouter({
|
||||
auth: authRouter,
|
||||
user: userRouter,
|
||||
order: orderRouter,
|
||||
emailAccounts: emailAccountsRouter,
|
||||
ckflow: ckflowRouter,
|
||||
coupon: couponRouter,
|
||||
product: productRouter,
|
||||
customerInfo: customerInfoRouter,
|
||||
});
|
||||
|
||||
export type Router = typeof router;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { getCustomerInfoUseCases } from "$lib/domains/customerinfo/usecases";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { PassengerInfoController } from "$lib/domains/passengerinfo/domain/controller";
|
||||
import { PassengerInfoRepository } from "$lib/domains/passengerinfo/data/repository";
|
||||
import { db } from "@pkg/db";
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
const sess = locals.session;
|
||||
if (!sess) {
|
||||
return redirect(302, "/auth/login");
|
||||
}
|
||||
const pc = new PassengerInfoController(new PassengerInfoRepository(db));
|
||||
const res = await pc.listAllPassengerInfos();
|
||||
const cu = getCustomerInfoUseCases();
|
||||
const res = await cu.getAllCustomerInfo();
|
||||
return { data: res.data ?? [], error: res.error };
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import Container from "$lib/components/atoms/container.svelte";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import PassengerinfoTable from "$lib/domains/passengerinfo/view/passengerinfo-table.svelte";
|
||||
import CustomerinfoTable from "$lib/domains/customerinfo/view/customerinfo-table.svelte";
|
||||
import { pageTitle } from "$lib/hooks/page-title.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import type { PageData } from "./$types";
|
||||
import { toast } from "svelte-sonner";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
pageTitle.set("Passenger Info");
|
||||
|
||||
@@ -31,6 +31,6 @@
|
||||
<Title size="h3">User Data</Title>
|
||||
</div>
|
||||
|
||||
<PassengerinfoTable data={data.data} />
|
||||
<CustomerinfoTable data={data.data} />
|
||||
</Container>
|
||||
</main>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import { PassengerInfoRepository } from "$lib/domains/passengerinfo/data/repository";
|
||||
import { db } from "@pkg/db";
|
||||
import { getCustomerInfoUseCases } from "$lib/domains/customerinfo/usecases";
|
||||
import { getError } from "@pkg/logger";
|
||||
import { ERROR_CODES } from "@pkg/result";
|
||||
import { PassengerInfoController } from "$lib/domains/passengerinfo/domain/controller";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
|
||||
export const load: PageServerLoad = async ({ params }) => {
|
||||
const uid = parseInt(params.uid);
|
||||
@@ -18,7 +16,5 @@ export const load: PageServerLoad = async ({ params }) => {
|
||||
}),
|
||||
};
|
||||
}
|
||||
return new PassengerInfoController(
|
||||
new PassengerInfoRepository(db),
|
||||
).getPassengerInfo(uid);
|
||||
return await getCustomerInfoUseCases().getAllCustomerInfo();
|
||||
};
|
||||
|
||||
@@ -1,30 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { adminSiteNavMap, CARD_STYLE } from "$lib/core/constants";
|
||||
import type { PageData } from "./$types";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import Container from "$lib/components/atoms/container.svelte";
|
||||
import * as Breadcrumb from "$lib/components/ui/breadcrumb/index.js";
|
||||
import { onMount } from "svelte";
|
||||
import Icon from "$lib/components/atoms/icon.svelte";
|
||||
import EmailIcon from "~icons/solar/letter-broken";
|
||||
import PhoneIcon from "~icons/solar/phone-broken";
|
||||
import PassportIcon from "~icons/solar/passport-broken";
|
||||
import LocationIcon from "~icons/solar/map-point-broken";
|
||||
import ClipboardIcon from "~icons/solar/clipboard-list-broken";
|
||||
import GenderIcon from "~icons/mdi/gender-male-female";
|
||||
import CalendarIcon from "~icons/solar/calendar-broken";
|
||||
import UserIdIcon from "~icons/solar/user-id-broken";
|
||||
import DocumentIcon from "~icons/solar/document-text-broken";
|
||||
import CubeIcon from "~icons/solar/box-minimalistic-broken";
|
||||
import PackageIcon from "~icons/solar/box-broken";
|
||||
import CreditCardIcon from "~icons/solar/wallet-money-broken";
|
||||
import CardUserIcon from "~icons/solar/user-id-linear";
|
||||
import CardNumberIcon from "~icons/solar/card-recive-broken";
|
||||
import CalendarCheckIcon from "~icons/solar/calendar-linear";
|
||||
import LockKeyIcon from "~icons/solar/lock-keyhole-minimalistic-broken";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import * as Breadcrumb from "$lib/components/ui/breadcrumb/index.js";
|
||||
import { adminSiteNavMap, CARD_STYLE } from "$lib/core/constants";
|
||||
import { capitalize } from "$lib/core/string.utils";
|
||||
import PinfoCard from "$lib/domains/passengerinfo/view/pinfo-card.svelte";
|
||||
import CinfoCard from "$lib/domains/customerinfo/view/cinfo-card.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import GenderIcon from "~icons/mdi/gender-male-female";
|
||||
import PackageIcon from "~icons/solar/box-broken";
|
||||
import CubeIcon from "~icons/solar/box-minimalistic-broken";
|
||||
import CalendarIcon from "~icons/solar/calendar-broken";
|
||||
import CalendarCheckIcon from "~icons/solar/calendar-linear";
|
||||
import CardNumberIcon from "~icons/solar/card-recive-broken";
|
||||
import ClipboardIcon from "~icons/solar/clipboard-list-broken";
|
||||
import DocumentIcon from "~icons/solar/document-text-broken";
|
||||
import EmailIcon from "~icons/solar/letter-broken";
|
||||
import LockKeyIcon from "~icons/solar/lock-keyhole-minimalistic-broken";
|
||||
import LocationIcon from "~icons/solar/map-point-broken";
|
||||
import PassportIcon from "~icons/solar/passport-broken";
|
||||
import PhoneIcon from "~icons/solar/phone-broken";
|
||||
import UserIdIcon from "~icons/solar/user-id-broken";
|
||||
import CardUserIcon from "~icons/solar/user-id-linear";
|
||||
import CreditCardIcon from "~icons/solar/wallet-money-broken";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
@@ -179,9 +179,9 @@
|
||||
class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-1 lg:grid-cols-3"
|
||||
>
|
||||
{#each piiData as { icon, title, value }}
|
||||
<PinfoCard {icon} {title}>
|
||||
<CinfoCard {icon} {title}>
|
||||
<p class="break-all font-medium">{value}</p>
|
||||
</PinfoCard>
|
||||
</CinfoCard>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,9 +195,9 @@
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 md:grid-cols-3">
|
||||
{#each addressInfo as { title, value }}
|
||||
<PinfoCard {title}>
|
||||
<CinfoCard {title}>
|
||||
<p class="font-medium">{value}</p>
|
||||
</PinfoCard>
|
||||
</CinfoCard>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,9 +212,9 @@
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{#each cardInfo as { icon, title, value }}
|
||||
<PinfoCard {icon} {title}>
|
||||
<CinfoCard {icon} {title}>
|
||||
<p class="break-all font-medium">{value}</p>
|
||||
</PinfoCard>
|
||||
</CinfoCard>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,14 +229,14 @@
|
||||
|
||||
<div class={`grid grid-cols-2 gap-4`}>
|
||||
{#if data.data?.orderId}
|
||||
<PinfoCard icon={PackageIcon} title="Order">
|
||||
<CinfoCard icon={PackageIcon} title="Order">
|
||||
<a
|
||||
href={`${adminSiteNavMap.orders}/${data.data.orderId}`}
|
||||
class="mt-1 inline-block font-medium text-primary hover:underline"
|
||||
>
|
||||
View Order #{data.data.orderId}
|
||||
</a>
|
||||
</PinfoCard>
|
||||
</CinfoCard>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user