diff --git a/App/app/(portal)/admin/companies/actions.ts b/App/app/(portal)/admin/companies/actions.ts new file mode 100644 index 0000000..bb05530 --- /dev/null +++ b/App/app/(portal)/admin/companies/actions.ts @@ -0,0 +1,97 @@ +"use server"; + +import { auth } from "@/auth"; +import { db } from "@/lib/db"; +import { hasPermission } from "@/lib/permissions"; +import { z } from "zod"; +import { revalidatePath } from "next/cache"; + +type ActionResult = { ok: true } | { error: string }; + +const companySchema = z.object({ + name: z.string().min(1, "Company name is required"), + gstNumber: z.string().optional(), + address: z.string().optional(), + telephone: z.string().optional(), + mobile: z.string().optional(), + email: z.string().email("Invalid email").optional().or(z.literal("")), + invoiceAddress: z.string().optional(), +}); + +export async function createCompany(formData: FormData): Promise { + const session = await auth(); + if (!session?.user || !hasPermission(session.user.role, "manage_vessels_accounts")) { + return { error: "Unauthorized" }; + } + + const parsed = companySchema.safeParse({ + name: formData.get("name"), + gstNumber: (formData.get("gstNumber") as string) || undefined, + address: (formData.get("address") as string) || undefined, + telephone: (formData.get("telephone") as string) || undefined, + mobile: (formData.get("mobile") as string) || undefined, + email: (formData.get("email") as string) || undefined, + invoiceAddress: (formData.get("invoiceAddress") as string) || undefined, + }); + if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" }; + + const { name, gstNumber, address, telephone, mobile, email, invoiceAddress } = parsed.data; + await db.company.create({ + data: { name, gstNumber: gstNumber ?? null, address: address ?? null, telephone: telephone ?? null, mobile: mobile ?? null, email: email || null, invoiceAddress: invoiceAddress ?? null }, + }); + revalidatePath("/admin/companies"); + return { ok: true }; +} + +export async function updateCompany(formData: FormData): Promise { + const session = await auth(); + if (!session?.user || !hasPermission(session.user.role, "manage_vessels_accounts")) { + return { error: "Unauthorized" }; + } + + const id = formData.get("id") as string; + if (!id) return { error: "Company ID is required" }; + + const parsed = companySchema.safeParse({ + name: formData.get("name"), + gstNumber: (formData.get("gstNumber") as string) || undefined, + address: (formData.get("address") as string) || undefined, + telephone: (formData.get("telephone") as string) || undefined, + mobile: (formData.get("mobile") as string) || undefined, + email: (formData.get("email") as string) || undefined, + invoiceAddress: (formData.get("invoiceAddress") as string) || undefined, + }); + if (!parsed.success) return { error: parsed.error.errors[0]?.message ?? "Validation failed" }; + + const { name, gstNumber, address, telephone, mobile, email, invoiceAddress } = parsed.data; + await db.company.update({ + where: { id }, + data: { name, gstNumber: gstNumber ?? null, address: address ?? null, telephone: telephone ?? null, mobile: mobile ?? null, email: email || null, invoiceAddress: invoiceAddress ?? null }, + }); + revalidatePath("/admin/companies"); + return { ok: true }; +} + +export async function deleteCompany(id: string): Promise { + const session = await auth(); + if (!session?.user || !hasPermission(session.user.role, "manage_vessels_accounts")) return { error: "Unauthorized" }; + + const inUse = await db.purchaseOrder.findFirst({ where: { companyId: id } }); + if (inUse) return { error: "Cannot delete: company is referenced in purchase orders." }; + + await db.company.delete({ where: { id } }); + revalidatePath("/admin/companies"); + return { ok: true }; +} + +export async function toggleCompanyActive(id: string): Promise { + const session = await auth(); + if (!session?.user || !hasPermission(session.user.role, "manage_vessels_accounts")) { + return { error: "Unauthorized" }; + } + const company = await db.company.findUnique({ where: { id }, select: { isActive: true } }); + if (!company) return { error: "Company not found" }; + await db.company.update({ where: { id }, data: { isActive: !company.isActive } }); + revalidatePath("/admin/companies"); + return { ok: true }; +} diff --git a/App/app/(portal)/admin/companies/companies-table.tsx b/App/app/(portal)/admin/companies/companies-table.tsx new file mode 100644 index 0000000..5094dab --- /dev/null +++ b/App/app/(portal)/admin/companies/companies-table.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { useState } from "react"; +import { AddCompanyButton, EditCompanyButton } from "./company-form"; +import { RowActionsMenu, RowActionsItem, RowActionsDestructiveItem, RowActionsSeparator } from "@/components/ui/row-actions-menu"; +import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog"; +import { ConfirmDialog } from "@/components/ui/confirm-dialog"; +import { deleteCompany, toggleCompanyActive } from "./actions"; + +export type CompanyRow = { + id: string; + name: string; + gstNumber: string | null; + address: string | null; + telephone: string | null; + mobile: string | null; + email: string | null; + invoiceAddress: string | null; + isActive: boolean; +}; + +function CompanyActionsMenu({ company }: { company: CompanyRow }) { + const [editOpen, setEditOpen] = useState(false); + const [deleteOpen, setDeleteOpen] = useState(false); + const [toggleOpen, setToggleOpen] = useState(false); + + return ( + <> + + setEditOpen(true)}>Edit + setToggleOpen(true)}> + {company.isActive ? "Deactivate" : "Activate"} + + + setDeleteOpen(true)}>Delete + + + deleteCompany(company.id)} + /> + toggleCompanyActive(company.id)} + /> + + ); +} + +export function CompaniesTable({ companies }: { companies: CompanyRow[] }) { + return ( +
+
+
+

Company Management

+

Sister companies used for invoicing and purchase orders

+
+ +
+ +
+ + + + + + + + + + + + {companies.length === 0 && ( + + + + )} + {companies.map((c) => ( + + + + + + + + ))} + +
Company NameGST NumberContactStatus
+ No companies yet. Add one to start selecting it on purchase orders. +
+

{c.name}

+ {c.address &&

{c.address}

} +
{c.gstNumber ?? β€”} + {c.telephone &&

☎ {c.telephone}

} + {c.mobile &&

πŸ“± {c.mobile}

} + {c.email &&

βœ‰ {c.email}

} + {!c.telephone && !c.mobile && !c.email && β€”} +
+ + {c.isActive ? "Active" : "Inactive"} + + + +
+
+
+ ); +} diff --git a/App/app/(portal)/admin/companies/company-form.tsx b/App/app/(portal)/admin/companies/company-form.tsx new file mode 100644 index 0000000..c0ac4a0 --- /dev/null +++ b/App/app/(portal)/admin/companies/company-form.tsx @@ -0,0 +1,148 @@ +"use client"; + +import { useState } from "react"; +import { useRouter } from "next/navigation"; +import { AdminDialog } from "@/components/ui/admin-dialog"; +import { createCompany, updateCompany } from "./actions"; + +type CompanyRow = { + id: string; + name: string; + gstNumber: string | null; + address: string | null; + telephone: string | null; + mobile: string | null; + email: string | null; + invoiceAddress: string | null; + isActive: boolean; +}; + +const INPUT = "w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-primary-500/20"; +const LABEL = "block text-xs font-medium text-neutral-700 mb-1"; + +function CompanyFormFields({ company }: { company?: CompanyRow }) { + return ( +
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +