enhance project move
This commit is contained in:
@@ -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");
|
||||
}}
|
||||
>
|
||||
<option value="">All Issues</option>
|
||||
<option value="PROJECT">Project Issues</option>
|
||||
<option value="PURCHASE_ORDER">Purchase Order Issues</option>
|
||||
currentVillaFilter = (e.target as HTMLSelectElement).value || null;
|
||||
fetchIssues(currentSearchTerm, currentVillaFilter);
|
||||
}}
|
||||
>
|
||||
<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,58 +805,60 @@
|
||||
</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">
|
||||
<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
|
||||
>
|
||||
➡️ PROJECT
|
||||
</button>
|
||||
</td>
|
||||
{: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
|
||||
>
|
||||
➡️ PURCHASE ORDER
|
||||
</button>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="px-4 py-2">
|
||||
{#if projectIssueMap.has(row.id)}
|
||||
<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-gray-400 px-3 py-1.5 text-white text-xs font-medium cursor-not-allowed"
|
||||
disabled
|
||||
>
|
||||
➡️ PROJECT
|
||||
✔ LINKED PROJECT
|
||||
</button>
|
||||
</td>
|
||||
{: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"
|
||||
disabled
|
||||
>
|
||||
➡️ PURCHASE ORDER
|
||||
</button>
|
||||
</td>
|
||||
{:else}
|
||||
<td class="px-4 py-2">
|
||||
{: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>
|
||||
<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,
|
||||
)}
|
||||
>
|
||||
➡️ PURCHASE ORDER
|
||||
</button>
|
||||
</td>
|
||||
{/if}
|
||||
{/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)}
|
||||
>
|
||||
➡️ 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}
|
||||
Reference in New Issue
Block a user