first commit

This commit is contained in:
Aji Setiaji
2025-05-27 21:43:01 +07:00
commit 8d984635af
30 changed files with 7314 additions and 0 deletions

View File

@@ -0,0 +1,618 @@
<script lang="ts">
import { onMount } from "svelte";
import { supabase } from "$lib/supabaseClient";
type Project = {
id: string;
issue_id: string;
project_number: string;
add_to_po: boolean;
input_by: string;
project_due_date: string;
picture_link: string;
};
type insetProject = {
issue_id: string;
input_by: string;
project_due_date: string;
picture_link: string;
};
type Projects = {
id: string;
name: string;
priority: string;
add_to_po: boolean;
description_of_the_issue: string;
picture_link: string;
need_approval: boolean;
area_of_villa: string;
input_by: string;
issue_number: string;
issue_id: string;
villa_name: string;
report_date: string;
project_due_date: string;
};
let allRows: Projects[] = [];
type columns = {
key: string;
title: string;
};
const columns: columns[] = [
{ key: "name", title: "Project Name" },
{ key: "priority", title: "Priority" },
{ key: "add_to_po", title: "Add to PO" },
{ key: "description_of_the_issue", title: "Description" },
{ key: "picture_link", title: "Picture Link" },
{ key: "need_approval", title: "Need Approval" },
{ key: "area_of_villa", title: "Area of Villa" },
{ key: "input_by", title: "Input By" },
{ key: "issue_number", title: "Issue Number" },
{ key: "villa_name", title: "Villa Name" },
{ key: "report_date", title: "Report Date" },
{ key: "project_due_date", title: "Project Due Date" },
{ key: "actions", title: "Actions" },
];
let selectedFile: File | null = null;
let imagePreviewUrl: string | null = null;
function handleFileChange(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
selectedFile = input.files[0];
imagePreviewUrl = URL.createObjectURL(selectedFile);
}
}
let currentPage = 1;
let rowsPerPage = 10;
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
$: paginatedRows = allRows.slice(
(currentPage - 1) * rowsPerPage,
currentPage * rowsPerPage,
);
function goToPage(page: number) {
if (page >= 1 && page <= totalPages) currentPage = page;
}
async function fetchProjects() {
// Fetch all projects
const { data, error } = await supabase
.from("projects")
.select("*")
.order("id", { ascending: false });
// ambil issue_id dari projects kemudian ambil data issue yang sesuai
const issueIds = data?.map((project: Project) => project.issue_id);
const { data: issueData, error: issueError } = await supabase
.from("issues")
.select("*")
.in("id", issueIds || [])
.order("id", { ascending: false });
// gabungkan data projects dengan data issues
if (!data || !issueData) {
console.error("Error fetching projects or issues:", error);
return;
}
// Set allRows to the combined data
allRows = data.map((project: Project) => {
const issue = issueData.find(
(issue) => issue.id === project.issue_id,
);
return {
...project,
id: project.id,
name: issue ? issue.name : "Unknown",
priority: issue ? issue.priority : "Unknown",
add_to_po: project.add_to_po,
description_of_the_issue: issue
? issue.description_of_the_issue
: "No description",
picture_link: project.picture_link,
need_approval: issue ? issue.need_approval : false,
area_of_villa: issue ? issue.area_of_villa : "Unknown",
input_by: project.input_by,
issue_number: issue ? issue.issue_number : "Unknown",
villa_name: issue ? issue.villa_name : "Unknown",
report_date: issue ? issue.reported_date : "Unknown",
project_due_date: project.project_due_date,
};
});
// hanya valid uuid
if (issueError) {
console.error("Error fetching available issues:", issueError);
return;
}
}
onMount(() => {
fetchProjects();
});
$: currentPage = 1; // Reset to first page when allRows changes
let showModal = false;
let isEditing = false;
let currentEditingId: string | null = null;
let newProjects: Record<string, any> = {};
let projectForUpdate: Record<string, any> | null = null;
let dataIssueIds: Record<string, any>[] = [];
const excludedKeys = [
"id",
"issue_id",
"number_project",
"input_by",
"name",
"priority",
"description_of_the_issue",
"need_approval",
"area_of_villa",
"villa_name",
"report_date",
"actions",
"add_to_po",
"issue_number",
];
const formColumns = columns.filter(
(col) => !excludedKeys.includes(col.key),
);
function openModal(project: Projects | null = null) {
if (project) {
isEditing = true;
currentEditingId = project.id;
newProjects = { ...project };
} else {
isEditing = false;
currentEditingId = null;
newProjects = {};
}
showModal = true;
fetchIssueIds(); // Fetch issue IDs when opening the modal
}
//validation project make
function validateProjectCheckBox(project: insetProject): boolean {
if (!project.project_due_date) {
console.error("Project due date is required");
alert("Project due date is required");
return false;
}
if (!project.picture_link) {
console.error("Picture link is required");
alert("Picture link is required");
return false;
}
if (
project.project_due_date &&
isNaN(Date.parse(project.project_due_date))
) {
console.error("Project due date must be a valid date");
alert("Project due date must be a valid date");
return false;
}
// if (
// project.picture_link &&
// !/^https?:\/\/.+\.(jpg|jpeg|png|gif)$/.test(project.picture_link)
// ) {
// console.error("Picture link must be a valid image URL");
// alert("Picture link must be a valid image URL");
// return false;
// }
return true;
}
//get all id dan name from issues
async function fetchIssueIds() {
const { data, error } = await supabase
.from("issues")
.select("id, name")
.order("id", { ascending: false });
if (error) {
console.error("Error fetching issues:", error);
return;
}
dataIssueIds = data.map((issue) => ({
id: issue.id,
name: issue.name,
}));
}
async function addToPo(project: Project) {
if (!project.add_to_po) {
console.error("Project must be added to PO");
alert("Project must be added to PO");
return;
}
// Validate project before saving
if (!validateProjectCheckBox(project)) {
return;
}
projectForUpdate = {
add_to_po: project?.add_to_po,
input_by: project?.input_by,
project_due_date: project?.project_due_date,
picture_link: project?.picture_link,
};
const { data, error } = await supabase
.from("projects")
.update(projectForUpdate)
.eq("id", currentEditingId);
if (error) {
console.error("Error updating project:", error);
return;
}
alert("Add To PO Successfully");
await fetchProjects();
// tambah ke Purchase Order
const { data: poData, error: poError } = await supabase
.from("purchase_orders")
.insert({
issue_id: project.issue_id,
po_status: "REQUESTED",
});
if (poError) {
console.error("Error adding to Purchase Order:", poError);
return;
}
}
async function saveProject(event: Event) {
const formData = new FormData(event.target as HTMLFormElement);
const projectUpdate: insetProject = {
issue_id: formData.get("issue_id") as string,
input_by: formData.get("input_by") as string,
project_due_date: formData.get("project_due_date") as string,
picture_link:
imagePreviewUrl || (formData.get("picture_link") as string),
};
// Validate project before saving
if (!validateProjectCheckBox(projectUpdate)) {
return;
}
console.log("current editing", currentEditingId);
if (isEditing && currentEditingId) {
const { data, error } = await supabase
.from("projects")
.update(projectUpdate)
.eq("id", currentEditingId);
if (error) {
console.error("Error updating project:", error);
return;
} else {
alert("Project updated successfully");
}
} else {
const { data, error } = await supabase
.from("projects")
.insert(projectUpdate);
if (error) {
console.error("Error inserting project:", error);
return;
}
}
await fetchProjects();
showModal = false;
}
async function deleteProject(id: string) {
const { error } = await supabase.from("projects").delete().eq("id", id);
if (error) {
console.error("Error deleting project:", error);
return;
}
await fetchProjects();
}
</script>
<div>
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<div>
<h2 class="text-lg font-semibold text-gray-800">Project List</h2>
<p class="text-sm text-gray-600">
Manage your projects here. You can add, edit, or delete
projects.
</p>
</div>
<button
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
on:click={() => openModal()}
>
Add Projects
</button>
</div>
<div class="overflow-x-auto rounded-lg shadow mb-4">
<table class="min-w-[1000px] divide-y divide-gray-200 text-sm w-max">
<thead class="bg-gray-100">
<tr>
{#each columns as col}
{#if col.key === "name"}
<th
class="sticky left-0 px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
style="background-color: #f0f8ff; z-index: 10;"
>
{col.title}
</th>
{:else}
<th
class="px-4 py-3 text-left font-semibold text-gray-700 uppercase tracking-wider whitespace-nowrap"
>
{col.title}
</th>
{/if}
{/each}
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{#each paginatedRows as row}
<tr class="hover:bg-gray-50 transition">
{#each columns as col}
{#if col.key === "name"}
<td
class="sticky left-0 px-4 py-2 font-medium text-blue-600"
style="background-color: #f0f8ff; cursor: pointer;"
>
{row[col.key]}
</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={() => deleteProject(row.id)}
>
🗑️ Delete
</button>
</td>
{:else if col.key === "add_to_po"}
<td class="px-4 py-2 text-center">
<input
type="checkbox"
checked={row.add_to_po}
on:change={async (e) => {
const isChecked = (
e.target as HTMLInputElement
).checked;
row.add_to_po = isChecked;
if (isChecked) {
// map to project
const project: Project = {
id: row.id,
issue_id: row.issue_id,
project_number:
row.issue_number,
add_to_po: isChecked,
input_by: row.input_by,
project_due_date:
row.project_due_date,
picture_link:
row.picture_link,
};
currentEditingId = row.id;
await addToPo(project);
}
}}
/>
</td>
{:else if col.key === "need_approval"}
<td class="px-4 py-2">
{#if row[col.key as keyof Projects]}
{:else}
{/if}
</td>
{:else if col.key === "project_picture_link"}
<td class="px-4 py-2">
{#if row.picture_link}
<a
href={row.picture_link}
target="_blank"
class="text-blue-600 hover:underline"
>View Picture</a
>
{:else}
No Picture
{/if}
</td>
{:else}
<td class="px-4 py-2 text-gray-700"
>{row[col.key as keyof Projects]}</td
>
{/if}
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
<!-- Pagination controls -->
<div class="flex justify-between items-center text-sm">
<div>
Showing {(currentPage - 1) * rowsPerPage + 1}
{Math.min(currentPage * rowsPerPage, allRows.length)} of {allRows.length}
</div>
<div class="space-x-2">
<button
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
on:click={() => goToPage(currentPage - 1)}
disabled={currentPage === 1}
>
Previous
</button>
{#each Array(totalPages)
.fill(0)
.map((_, i) => i + 1) as page}
<button
class="px-3 py-1 rounded border text-sm
{currentPage === page
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white border-gray-300 hover:bg-gray-100'}"
on:click={() => goToPage(page)}
>
{page}
</button>
{/each}
<button
class="px-3 py-1 rounded border border-gray-300 bg-white hover:bg-gray-100 disabled:opacity-50"
on:click={() => goToPage(currentPage + 1)}
disabled={currentPage === totalPages}
>
Next
</button>
</div>
</div>
</div>
{#if showModal}
<div
class="fixed inset-0 flex items-center justify-center bg-gray-800 bg-opacity-50 z-50"
>
<div
class="bg-white p-6 rounded shadow-lg w-[600px] max-h-[90vh] overflow-y-auto space-y-4"
>
<h3 class="text-lg font-semibold mb-4">
{isEditing ? "Edit Project" : "Add Project"}
</h3>
<form on:submit|preventDefault={saveProject}>
{#each formColumns as col}
{#if col.key === "project_due_date"}
<div class="mb-4">
<label
for={col.key}
class="block text-sm font-medium text-gray-700 mb-1"
>
{col.title}
</label>
<input
name={col.key}
type="date"
id={col.key}
bind:value={newProjects[col.key]}
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
/>
</div>
{:else if col.key === "picture_link"}
<!-- image upload -->
<div class="mb-4">
<label
for={col.key}
class="block text-sm font-medium text-gray-700 mb-1"
>
{col.title}
</label>
<input
name="picture_link"
class="w-full border px-3 py-2 rounded"
type="file"
accept="image/*"
on:change={handleFileChange}
/>
<p class="text-xs text-gray-500">
Upload an image related to the issue.
</p>
{#if imagePreviewUrl}
<img
src={imagePreviewUrl}
alt="Preview"
class="mt-2 max-h-48 rounded border"
/>
{/if}
</div>
{:else}
<div class="mb-4">
<label
for={col.key}
class="block text-sm font-medium text-gray-700 mb-1"
>
{col.title}
</label>
<input
name={col.key}
type="text"
id={col.key}
bind:value={newProjects[col.key]}
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
/>
</div>
{/if}
{/each}
<!-- list issue dan bisa mencari -->
<div class="mb-4">
<label
for="issue_id"
class="block text-sm font-medium text-gray-700 mb-1"
>
Issue ID
</label>
<select
id="issue_id"
name="issue_id"
required
bind:value={newProjects.issue_id}
class="w-full px-3 py-2 border border-gray-300 rounded focus:outline-none focus:ring focus:ring-blue-500"
>
<option value="" disabled selected>Select Issue</option>
{#each dataIssueIds as issueId}
<option value={issueId.id}>{issueId.name}</option>
{/each}
</select>
</div>
<div class="flex justify-end space-x-2">
<button
type="button"
class="px-4 py-2 bg-gray-200 text-gray-700 rounded hover:bg-gray-300"
on:click={() => (showModal = false)}
>
Cancel
</button>
<button
type="submit"
class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Save
</button>
</div>
</form>
</div>
</div>
{/if}