Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "SignOff" ADD COLUMN "partnerSignatureUrl" TEXT,
ADD COLUMN "partnerSignerName" TEXT;
2 changes: 2 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,10 @@ model SignOff {
staffMemberName String
partnerId Int
partnerName String
partnerSignerName String?
date DateTime
signatureUrl String?
partnerSignatureUrl String?
createdAt DateTime @default(now())
Expand Down
56 changes: 56 additions & 0 deletions src/app/api/donorOffers/[donorOfferId]/csv/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { auth } from "@/auth";
import DonorOfferService from "@/services/donorOfferService";
import UserService from "@/services/userService";
import {
ArgumentError,
AuthenticationError,
errorResponse,
} from "@/util/errors";
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";

const paramSchema = z.object({
donorOfferId: z.string().transform((val) => {
const parsed = parseInt(val, 10);
if (Number.isNaN(parsed)) {
throw new Error("donorOfferId must be a valid number");
}
return parsed;
}),
});

export async function GET(
_: NextRequest,
{ params }: { params: Promise<{ donorOfferId: string }> }
) {
try {
const session = await auth();
if (!session?.user) {
throw new AuthenticationError("Session required");
}

UserService.checkPermission(session.user, "offerWrite");

const resolvedParams = await params;
const parsed = paramSchema.safeParse(resolvedParams);
if (!parsed.success) {
throw new ArgumentError(parsed.error.message);
}

const { csv, fileName } =
await DonorOfferService.getUnfinalizedDonorOfferCsv(
parsed.data.donorOfferId
);

return new NextResponse(csv, {
status: 200,
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": `attachment; filename="${fileName}"`,
"Cache-Control": "no-store",
},
});
} catch (error) {
return errorResponse(error);
}
}
16 changes: 9 additions & 7 deletions src/app/api/reset/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,22 @@ async function resetDatabase() {
const signOffs = await db.signOff.findMany({
select: {
signatureUrl: true,
partnerSignatureUrl: true,
},
});

// Delete signatures from Azure Storage
for (const signOff of signOffs) {
if (signOff.signatureUrl) {
const signatureUrls = [
signOff.signatureUrl,
signOff.partnerSignatureUrl,
].filter(Boolean);

for (const signatureUrl of signatureUrls) {
try {
await FileService.deleteSignature(signOff.signatureUrl);
await FileService.deleteSignature(signatureUrl!);
} catch (error) {
console.warn(
`Failed to delete signature: ${signOff.signatureUrl}`,
error
);
console.warn(`Failed to delete signature: ${signatureUrl}`, error);
}
}
}
Expand Down Expand Up @@ -75,4 +78,3 @@ export async function POST() {
return errorResponse(error);
}
}

13 changes: 13 additions & 0 deletions src/app/api/signOffs/[signOffId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ const patchSchema = z.object({
staffName: z.string().optional(),
partnerId: z.number().int().positive().optional(),
partnerName: z.string().optional(),
partnerSignerName: z.string().optional(),
date: z
.string()
.transform((str) => new Date(str))
.optional(),
signatureUrl: z.string().optional(),
partnerSignatureUrl: z.string().optional(),
allocations: z.array(z.number().int().positive()).optional(),
staffUserId: z.number().int().positive().optional(),
});

export async function GET(
Expand Down Expand Up @@ -85,6 +88,7 @@ export async function PATCH(
}

const form = await request.formData();
const userId = parseInt(session.user.id!);
const formObj = {
staffName:
form.get("staffName") !== null
Expand All @@ -98,15 +102,24 @@ export async function PATCH(
form.get("partnerName") !== null
? String(form.get("partnerName"))
: undefined,
partnerSignerName:
form.get("partnerSignerName") !== null
? String(form.get("partnerSignerName"))
: undefined,
date: form.get("date") !== null ? String(form.get("date")) : undefined,
signatureUrl:
form.get("signatureUrl") !== null
? String(form.get("signatureUrl"))
: undefined,
partnerSignatureUrl:
form.get("partnerSignatureUrl") !== null
? String(form.get("partnerSignatureUrl"))
: undefined,
allocations:
form.getAll("allocation").length > 0
? form.getAll("allocation").map((val) => Number(val))
: undefined,
staffUserId: userId,
};
const parsed = patchSchema.safeParse(formObj);

Expand Down
11 changes: 11 additions & 0 deletions src/app/api/signOffs/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ const createSignOffSchema = z.object({
partnerId: z.number().int().positive(),
staffName: z.string(),
partnerName: z.string(),
partnerSignerName: z.string(),
date: z.string().transform((str) => new Date(str)),
signatureUrl: z.string(),
partnerSignatureUrl: z.string(),
allocations: z.array(z.number()),
staffUserId: z.number().int().positive().optional(),
});
Expand Down Expand Up @@ -85,6 +87,15 @@ export async function POST(req: NextRequest) {
? String(form.get("partnerName"))
: undefined,
date: form.get("date") != null ? String(form.get("date")) : undefined,
partnerSignerName:
form.get("partnerSignerName") != null
? String(form.get("partnerSignerName"))
: undefined,
partnerSignatureUrl:
form.get("partnerSignatureUrl") != null
? String(form.get("partnerSignatureUrl"))
: undefined,

signatureUrl:
form.get("signatureUrl") != null
? String(form.get("signatureUrl"))
Expand Down
10 changes: 8 additions & 2 deletions src/components/NotificationHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,13 @@ export default function NotificationHandler({
const router = useRouter();
const [client, setClient] = useState<Ably.Realtime | null>(null);
const [notifications, setNotifications] = useState<UnifiedNotification[]>([]);
const activeSupportTab = searchParams.get("activeTab");

const [chatUnreadCount, setChatUnreadCount] = useState(0);

// Ensure that mirroring doesn't overwrite sessionStorage before loading
const isHydrated = useRef(false);
const activeSupportTabRef = useRef(activeSupportTab);

useEffect(() => {
const realtime = getRealtimeClient();
Expand Down Expand Up @@ -129,6 +131,10 @@ export default function NotificationHandler({
[apiClient, notifications]
);

useEffect(() => {
activeSupportTabRef.current = activeSupportTab;
}, [activeSupportTab]);

useEffect(() => {
if (!user) {
sessionStorage.removeItem("chat_notifications");
Expand Down Expand Up @@ -293,7 +299,7 @@ export default function NotificationHandler({
const channelId = event.channel?.id ?? event.cid?.split(":")[1];
if (!channelId) return;

if (searchParams.get("activeTab") === "Unresolved") return;
if (activeSupportTabRef.current === "Unresolved") return;

const text = event.message?.text?.trim();
const channelName =
Expand Down Expand Up @@ -357,7 +363,7 @@ export default function NotificationHandler({

return () => {
didInterrupt = true;
streamClient.off("notification.message_new", handleTicketMessage);
streamClient.off("message.new", handleTicketMessage);
streamClient.off("notification.mark_read", handleMarkRead);
streamClient
.disconnectUser()
Expand Down
Loading
Loading