enhance project move

This commit is contained in:
2025-06-25 01:34:17 +08:00
parent d9284becc1
commit f6f671879c

View File

@@ -148,11 +148,38 @@
id: string;
employee_name: string;
};
let currentVillaFilter: string | null = null;
let currentSearchTerm: string | null = null;
let showProjectModal = false;
let selectedIssueId: string | null = null;
let newProject = {
project_name: "",
issue_id: "",
input_by: "",
project_due_date: "",
assigned_to: "",
project_comment: ""
};
let dataVilla: Villa[] = [];
let dataUser: User[] = [];
let projectIssueMap: Set<string> = new Set();
async function fetchExistingProjectLinks() {
const { data, error } = await supabase
.from("vb_projects")
.select("issue_id");
if (!error && data) {
projectIssueMap = new Set(data.map((p) => p.issue_id));
} else {
console.error("Error loading existing projects:", error);
}
}
onMount(async () => {
await fetchExistingProjectLinks();
const { data, error } = await supabase
.from("vb_villas")
.select("id, villa_name, villa_status")
@@ -251,9 +278,9 @@
];
async function fetchIssues(
filter: string | null = null,
search: string | null = null,
sort: string | null = null,
villaNameFilter: string | null = null,
sort: string | null = "created_at",
order: "asc" | "desc" = "desc",
offset: number = 0,
limit: number = 10,
@@ -264,8 +291,11 @@
.order(sort || "created_at", { ascending: order === "asc" })
.range(offset, offset + limit - 1);
if (filter) {
query = query.eq("move_issue", filter);
if (villaNameFilter) {
const villa = dataVilla.find(v => v.villa_name === villaNameFilter);
if (villa) {
query = query.eq("villa_id", villa.id);
}
}
if (search) {
@@ -399,7 +429,9 @@
}
if (isEditing && currentEditingId) {
newIssue = formColumns.reduce(
const session = await supabase.auth.getSession();
const userId = session.data.session?.user.id;
const reducedIssue = formColumns.reduce(
(acc, col) => {
if (col.key in newIssue) {
acc[col.key] = newIssue[col.key];
@@ -409,9 +441,13 @@
{} as Record<string, any>,
);
// Add system fields
reducedIssue.updated_by = userId;
reducedIssue.updated_at = new Date().toISOString();
const { error } = await supabase
.from("vb_issues")
.update(newIssue)
.update(reducedIssue)
.eq("id", currentEditingId);
if (error) {
@@ -439,8 +475,7 @@
"guest_communication",
) as string,
resolution: formData.get("resolution") as string,
need_approval:
formData.get("need_approval") === "false" ? true : false,
need_approval: newIssue.need_approval,
};
const { error } = await supabase
@@ -559,6 +594,82 @@
await fetchIssues();
}
// open project modal
let selectedIssueSummary = "";
function openProjectModal(issue: Issue) {
const dueDate = new Date(issue.created_at);
dueDate.setDate(dueDate.getDate() + 2);
selectedIssueSummary = issue.description_of_the_issue ?? "";
newProject = {
project_name: "",
issue_id: issue.id,
input_by: "", // can be prefilled if desired
project_due_date: dueDate.toISOString().split("T")[0], // mm/dd/yyyy
assigned_to: "",
project_comment: ""
};
showProjectModal = true;
}
async function submitProject() {
if (!newProject.project_name || !newProject.input_by || !newProject.assigned_to) {
alert("Please fill all required fields");
return;
}
// Prevent duplicate
const { data: existing, error: checkError } = await supabase
.from("vb_projects")
.select("id")
.eq("issue_id", newProject.issue_id)
.maybeSingle();
if (existing) {
alert("This issue already has a project assigned.");
return;
}
// Insert into Supabase
const { data, error } = await supabase.from("vb_projects").insert({
project_name: newProject.project_name,
issue_id: newProject.issue_id,
input_by: newProject.input_by,
project_due_date: new Date(newProject.project_due_date).toISOString(),
assigned_to: newProject.assigned_to,
project_comment: newProject.project_comment
}).select().single(); // grab inserted data
if (error) {
console.error("Insert error:", error);
alert("Failed to create project.");
return;
}
// 🔥 Fire webhook
try {
await fetch("https://flow.catalis.app/webhook-test/vb-project-new", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(data) // or customize the payload as needed
});
} catch (webhookError) {
console.error("Webhook error:", webhookError);
alert("Project saved, but failed to notify webhook.");
}
projectIssueMap = new Set([...projectIssueMap, newProject.issue_id]);
alert("Project created successfully.");
showProjectModal = false;
}
// insert id issue to purchase order
async function moveIssueToPurchaseOrder(issueId: string) {
// get user id from session
@@ -611,30 +722,43 @@
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
<input
type="text"
placeholder="🔍 Search by name..."
id="issue-search"
placeholder="🔍 Search by Issue..."
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-64 transition"
on:input={(e) => {
const searchTerm = (
e.target as HTMLInputElement
).value.toLowerCase();
fetchIssues(null, searchTerm, "created_at", "desc");
currentSearchTerm = (e.target as HTMLInputElement).value.toLowerCase();
fetchIssues( currentSearchTerm, currentVillaFilter);
}}
/>
<select
id="villa-filter"
class="border border-gray-300 focus:ring-2 focus:ring-blue-500 focus:outline-none px-4 py-2 rounded-xl text-sm w-48 transition"
on:change={(e) => {
const filter = (e.target as HTMLSelectElement).value;
fetchIssues(filter, null, null, "desc");
currentVillaFilter = (e.target as HTMLSelectElement).value || null;
fetchIssues(currentSearchTerm, currentVillaFilter);
}}
>
<option value="">All Issues</option>
<option value="PROJECT">Project Issues</option>
<option value="PURCHASE_ORDER">Purchase Order Issues</option>
<option value="">All Villas</option>
{#each dataVilla as villa}
<option value={villa.villa_name}>{villa.villa_name}</option>
{/each}
</select>
<button
class="bg-gray-200 text-gray-700 px-4 py-2 rounded-xl hover:bg-gray-300 text-sm transition"
on:click={() =>
fetchIssues(null, null, "created_at", "desc", 0, 10)}
on:click={() =>{
currentVillaFilter = null;
currentSearchTerm = null;
const searchInput = document.getElementById("issue-search") as HTMLInputElement;
if (searchInput) searchInput.value = "";
const moveSelect = document.getElementById("move-filter") as HTMLSelectElement;
if (moveSelect) moveSelect.value = "";
const villaSelect = document.getElementById("villa-filter") as HTMLSelectElement;
if (villaSelect) villaSelect.value = "";
fetchIssues(null, null, null);
}}
>
🔄 Reset
</button>
@@ -681,24 +805,20 @@
</td>
{:else if col.key === "actions"}
<td class="px-4 py-2">
<button
class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700"
on:click={() => openModal(row)}
>
✏️ Edit
</button>
<button
class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700"
on:click={() => deleteIssue(row.id)}
>
🗑️ Delete
</button>
{#if projectIssueMap.has(row.id)}
<button class="text-gray-400 cursor-not-allowed" disabled title="Cannot edit: linked to a project">✏️ Edit</button>
<button class="text-gray-400 cursor-not-allowed" disabled title="Cannot delete: linked to a project">🗑️ Delete</button>
{:else}
<button class="inline-flex items-center gap-1 rounded bg-blue-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-blue-700" on:click={() => openModal(row)}>✏️ Edit</button>
<button class="inline-flex items-center gap-1 rounded bg-red-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-red-700" on:click={() => deleteIssue(row.id)}>🗑️ Delete</button>
{/if}
</td>
{:else if col.key === "move_issue"}
{#if row[col.key as keyof Issue] === "PROJECT"}
<td class="px-4 py-2">
<button
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700 disabled:opacity-90"
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700"
disabled
>
➡️ PROJECT
@@ -707,7 +827,7 @@
{:else if row[col.key as keyof Issue] === "PURCHASE_ORDER"}
<td class="px-4 py-2">
<button
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700 disabled:opacity-90"
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700"
disabled
>
➡️ PURCHASE ORDER
@@ -715,24 +835,30 @@
</td>
{:else}
<td class="px-4 py-2">
{#if projectIssueMap.has(row.id)}
<button
class="inline-flex items-center gap-1 rounded bg-gray-400 px-3 py-1.5 text-white text-xs font-medium cursor-not-allowed"
disabled
>
✔ LINKED PROJECT
</button>
{:else}
<button
class="inline-flex items-center gap-1 rounded bg-green-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-green-700"
on:click={() =>
moveIssueToProject(row.id)}
on:click={() => openProjectModal(row)}
>
➡️ PROJECT
</button>
{/if}
<button
class="inline-flex items-center gap-1 rounded bg-yellow-600 px-3 py-1.5 text-white text-xs font-medium hover:bg-yellow-700"
on:click={() =>
moveIssueToPurchaseOrder(
row.id,
)}
on:click={() => moveIssueToPurchaseOrder(row.id)}
>
➡️ PURCHASE ORDER
</button>
</td>
{/if}
{:else if col.key === "guest_has_aggreed_issue_has_been_resolved"}
<td class="px-4 py-2">
{#if row[col.key as keyof Issue]}
@@ -946,15 +1072,17 @@
<div class="space-y-1">
<label class="block text-sm font-medium text-gray-700">
Need Approval
<select
name="need_approval"
class="w-full border px-3 py-2 rounded"
bind:value={newIssue[col.key as keyof Issue]}
>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
<input
type="checkbox"
bind:checked={newIssue.need_approval}
class="form-checkbox"
/>
</label>
<p class="text-sm text-gray-500 ml-1">
{newIssue.need_approval
? "✅ Approval Required"
: "❌ No Approval Needed"}
</p>
</div>
{:else if col.key === "issue_source"}
<div class="space-y-1">
@@ -1198,3 +1326,83 @@
</form>
</div>
{/if}
{#if showProjectModal}
<div class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
<form
on:submit|preventDefault={submitProject}
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
>
<h2 class="text-lg font-semibold">Create Project from Issue</h2>
{#if selectedIssueSummary}
<div class="text-sm text-gray-600 italic mb-2">
Related Issue: “{selectedIssueSummary}
</div>
{/if}
<label class="block text-sm font-medium text-gray-700">
Project Name *
<input
type="text"
bind:value={newProject.project_name}
class="w-full border px-3 py-2 rounded"
required
/>
</label>
<label class="block text-sm font-medium text-gray-700">
Assigned To
<select bind:value={newProject.assigned_to} class="w-full border px-3 py-2 rounded">
<option value="">-- Select Employee --</option>
{#each dataUser as emp}
<option value={emp.id}>{emp.employee_name}</option>
{/each}
</select>
</label>
<label class="block text-sm font-medium text-gray-700">
Input By
<select bind:value={newProject.input_by} class="w-full border px-3 py-2 rounded">
<option value="">-- Select Employee --</option>
{#each dataUser as emp}
<option value={emp.id}>{emp.employee_name}</option>
{/each}
</select>
</label>
<label class="block text-sm font-medium text-gray-700">
Project Due Date
<input
type="date"
bind:value={newProject.project_due_date}
class="w-full border px-3 py-2 rounded"
/>
</label>
<label class="block text-sm font-medium text-gray-700">
Comment
<textarea
bind:value={newProject.project_comment}
class="w-full border px-3 py-2 rounded"
rows="2"
></textarea>
</label>
<div class="flex justify-end gap-2">
<button
type="button"
class="px-4 py-2 text-sm rounded bg-gray-300 text-black hover:bg-gray-400"
on:click={() => showProjectModal = false}
>
Cancel
</button>
<button
type="submit"
class="px-4 py-2 text-sm rounded bg-blue-600 text-white hover:bg-blue-700"
>
Submit
</button>
</div>
</form>
</div>
{/if}