diff options
| author | joonhoekim <26rote@gmail.com> | 2025-03-25 15:55:45 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-03-25 15:55:45 +0900 |
| commit | 1a2241c40e10193c5ff7008a7b7b36cc1d855d96 (patch) | |
| tree | 8a5587f10ca55b162d7e3254cb088b323a34c41b /lib/po/service_r1.ts | |
initial commit
Diffstat (limited to 'lib/po/service_r1.ts')
| -rw-r--r-- | lib/po/service_r1.ts | 282 |
1 files changed, 282 insertions, 0 deletions
diff --git a/lib/po/service_r1.ts b/lib/po/service_r1.ts new file mode 100644 index 00000000..64af73c4 --- /dev/null +++ b/lib/po/service_r1.ts @@ -0,0 +1,282 @@ +"use server" + +import db from "@/db/db" +import { GetPOSchema } from "./validations" +import { unstable_cache } from "@/lib/unstable-cache"; +import { filterColumns } from "@/lib/filter-columns"; +import { asc, desc, ilike, inArray, and, gte, lte, not, or, eq, count} from "drizzle-orm"; +import { countPos, selectPos } from "./repository"; + +import { contractEnvelopes, contractsDetailView, contractSigners,contracts } from "@/db/schema/contract"; +import { revalidatePath } from "next/cache"; +import * as z from "zod" +import { vendorContacts } from "@/db/schema/vendors"; + +/** + * PQ 목록 조회 + */ +export async function getPOs(input: GetPOSchema) { + return unstable_cache( + async () => { + try { + const offset = (input.page - 1) * input.perPage; + + // 1. Try a simple query first to make sure the view works at all + try { + const testQuery = await db.select({ count: count() }) + .from(contractsDetailView); + console.log("Test query result:", testQuery); + } catch (testErr) { + console.error("Test query failed:", testErr); + } + + // 2. Build where clause with more careful handling + let advancedWhere; + try { + advancedWhere = filterColumns({ + table: contractsDetailView, + filters: input.filters, + joinOperator: input.joinOperator, + }); + console.log("Advanced where clause built successfully"); + } catch (whereErr) { + console.error("Error building advanced where:", whereErr); + advancedWhere = undefined; + } + + let globalWhere; + if (input.search) { + try { + const s = `%${input.search}%`; + globalWhere = or( + ilike(contractsDetailView.contractNo, s), + ilike(contractsDetailView.contractName, s), + ); + console.log("Global where clause built successfully"); + } catch (searchErr) { + console.error("Error building search where:", searchErr); + globalWhere = undefined; + } + } + + // 3. Combine where clauses safely + let finalWhere; + if (advancedWhere && globalWhere) { + finalWhere = and(advancedWhere, globalWhere); + } else { + finalWhere = advancedWhere || globalWhere; + } + + + // 4. Build order by + let orderBy; + try { + orderBy = input.sort.length > 0 + ? input.sort.map((item) => + item.desc ? desc(contractsDetailView[item.id]) : asc(contractsDetailView[item.id]) + ) + : [asc(contractsDetailView.createdAt)]; + } catch (orderErr) { + console.error("Error building order by:", orderErr); + orderBy = [asc(contractsDetailView.createdAt)]; + } + + // 5. Execute queries with proper error handling + let data = []; + let total = 0; + + try { + // Try without transaction first for better error visibility + const queryBuilder = db.select() + .from(contractsDetailView); + + // Add where clause if it exists + if (finalWhere) { + queryBuilder.where(finalWhere); + } + + // Add ordering + queryBuilder.orderBy(...orderBy); + + // Add pagination + queryBuilder.offset(offset).limit(input.perPage); + + // Execute query + data = await queryBuilder; + + // Get total count + const countBuilder = db.select({ count: count() }) + .from(contractsDetailView); + + if (finalWhere) { + countBuilder.where(finalWhere); + } + + const countResult = await countBuilder; + total = countResult[0]?.count || 0; + + } catch (queryErr) { + console.error("Query execution failed:", queryErr); + throw queryErr; // Rethrow to be caught by the outer try/catch + } + + const pageCount = Math.ceil(total / input.perPage); + + return { data, pageCount }; + } catch (err) { + // More detailed error logging + console.error("Error in getPOs:", err); + if (err instanceof Error) { + console.error("Error message:", err.message); + console.error("Error stack:", err.stack); + } + return { data: [], pageCount: 0 }; + } + }, + [JSON.stringify(input)], + { + revalidate: 3600, + tags: [`po`], + } + )(); +} + +// Schema for a single signer +const signerSchema = z.object({ + signerEmail: z.string().email(), + signerName: z.string().min(1), + signerPosition: z.string(), + signerType: z.enum(["REQUESTER", "VENDOR"]), + vendorContactId: z.number().optional(), + }); + + // Schema for the entire request + const signatureRequestSchema = z.object({ + contractId: z.number(), + signers: z.array(signerSchema).min(1, "At least one signer is required") + }); + + /** + * Server action to request electronic signatures for a contract from multiple parties + */ + export async function requestSignatures( + input: z.infer<typeof signatureRequestSchema> + ): Promise<{ success: boolean; message: string }> { + try { + // Validate the input + const validatedData = signatureRequestSchema.parse(input); + + // Use a transaction to ensure data consistency + return await db.transaction(async (tx) => { + // Get contract details using standard select + const [contract] = await tx + .select() + .from(contracts) + .where(eq(contracts.id, validatedData.contractId)) + .limit(1); + + if (!contract) { + throw new Error(`Contract with ID ${validatedData.contractId} not found`); + } + + // Generate unique envelope ID + const envelopeId = `env-${Date.now()}-${Math.floor(Math.random() * 1000)}`; + + // Get contract number or fallback + const contractNo = contract.contractNo || `contract-${validatedData.contractId}`; + + // Create a single envelope for all signers + const [newEnvelope] = await tx.insert(contractEnvelopes) + .values({ + contractId: validatedData.contractId, + envelopeId: envelopeId, + envelopeStatus: "sent", + fileName: `${contractNo}-signature.pdf`, // Required field + filePath: `/contracts/${validatedData.contractId}/signatures/${envelopeId}.pdf`, // Required field + // Add any other required fields based on your schema + }) + .returning(); + + // Check for duplicate emails + const signerEmails = new Set(); + for (const signer of validatedData.signers) { + if (signerEmails.has(signer.signerEmail)) { + throw new Error(`Duplicate signer email: ${signer.signerEmail}`); + } + signerEmails.add(signer.signerEmail); + } + + // Create signer records for each signer + for (const signer of validatedData.signers) { + await tx.insert(contractSigners) + .values({ + envelopeId: newEnvelope.id, + signerEmail: signer.signerEmail, + signerName: signer.signerName, + signerPosition: signer.signerPosition, + signerStatus: "sent", + signerType: signer.signerType, + // Only include vendorContactId if it's provided and the signer is a vendor + ...(signer.vendorContactId && signer.signerType === "VENDOR" + ? { vendorContactId: signer.vendorContactId } + : {}) + }); + } + + // Update contract status to indicate pending signatures + await tx.update(contracts) + .set({ status: "PENDING_SIGNATURE" }) + .where(eq(contracts.id, validatedData.contractId)); + + // In a real implementation, you would send the envelope to DocuSign or similar service + // For example: + // const docusignResult = await docusignClient.createEnvelope({ + // recipients: validatedData.signers.map(signer => ({ + // email: signer.signerEmail, + // name: signer.signerName, + // recipientType: signer.signerType === "REQUESTER" ? "signer" : "cc", + // routingOrder: signer.signerType === "REQUESTER" ? 1 : 2, + // })), + // documentId: `contract-${validatedData.contractId}`, + // // other DocuSign-specific parameters + // }); + + // Revalidate the path to refresh the data + revalidatePath("/po"); + + // Return success response + return { + success: true, + message: `Signature requests sent to ${validatedData.signers.length} recipient(s)` + }; + }); + } catch (error) { + console.error("Error requesting electronic signatures:", error); + return { + success: false, + message: error instanceof Error ? error.message : "Failed to send signature requests" + }; + } + } + + export async function getVendorContacts(vendorId: number) { + try { + const contacts = await db + .select({ + id: vendorContacts.id, + contactName: vendorContacts.contactName, + contactEmail: vendorContacts.contactEmail, + contactPosition: vendorContacts.contactPosition, + contactPhone: vendorContacts.contactPhone, + isPrimary: vendorContacts.isPrimary, + }) + .from(vendorContacts) + .where(eq(vendorContacts.vendorId, vendorId)) + .orderBy(vendorContacts.isPrimary, vendorContacts.contactName); + + return contacts; + } catch (error) { + console.error("Error fetching vendor contacts:", error); + throw new Error("Failed to fetch vendor contacts"); + } + }
\ No newline at end of file |
