penambahan fitur upload

This commit is contained in:
Aji Setiaji
2025-05-29 16:17:21 +07:00
parent 8d984635af
commit 5845704f1f
8 changed files with 1979 additions and 119 deletions

View File

@@ -128,7 +128,7 @@
});
type Issue = {
id: number;
id: string;
name: string;
villa_name: string;
area_of_villa: string;
@@ -170,6 +170,10 @@
};
let allRows: Issue[] = [];
let offset = 0;
let limit = 10;
let totalItems = 0;
export let formErrors = writable<{ [key: string]: string }>({});
type columns = {
key: string;
@@ -202,14 +206,40 @@
{ key: "actions", title: "Actions" },
];
async function fetchIssues() {
const { data: issues, error: issueError } = await supabase
async function fetchIssues(
filter: string | null = null,
search: string | null = null,
sort: string | null = null,
order: "asc" | "desc" = "desc",
offset: number = 0,
limit: number = 10,
) {
let query = supabase
.from("issues")
.select("*")
.order("created_at", { ascending: false });
.select("*", { count: "exact" })
.order(sort || "created_at", { ascending: order === "asc" })
.range(offset, offset + limit - 1);
if (issueError) {
console.error("Error fetching issues:", issueError);
if (filter) {
query = query.eq("move_issue", filter);
}
if (search) {
query = query.ilike("name", `%${search}%`);
}
const { data: issues, error, count } = await query;
if (error) {
console.error("Error fetching issues:", error);
return;
}
if (count !== undefined) {
totalItems = count ?? 0;
}
if (!issues || issues.length === 0) {
allRows = [];
return;
}
@@ -233,13 +263,10 @@
villas.find((v) => v.id === issue.villa_name).name || null,
}));
}
let currentPage = 1;
let rowsPerPage = 5;
$: totalPages = Math.ceil(allRows.length / rowsPerPage);
$: paginatedRows = allRows.slice(
(currentPage - 1) * rowsPerPage,
currentPage * rowsPerPage,
);
let currentPage = offset + 1;
let rowsPerPage = limit;
$: totalPages = Math.ceil(totalItems / rowsPerPage);
function editIssue(id: number) {
alert(`Edit issue with ID ${id}`);
@@ -250,12 +277,9 @@
}
onMount(() => {
fetchIssues();
fetchIssues(null, null, "created_at", "desc", offset, limit);
});
// Initialize the first page
$: currentPage = 1;
let showModal = false;
let isEditing = false;
let currentEditingId: string | null = null;
@@ -295,6 +319,25 @@
return;
}
//upload image if selected
if (selectedFile) {
const { data, error } = await supabase.storage
.from("villabugis")
.upload(
`issues/${Date.now()}_${selectedFile.name}`,
selectedFile,
);
console.log("Image upload data:", data);
if (error) {
alert("Error uploading image: " + error.message);
console.error("Error uploading image:", error);
return;
}
newIssue.issue_related_image = data.path;
}
if (isEditing && currentEditingId) {
const { error } = await supabase
.from("issues")
@@ -344,11 +387,18 @@
return;
}
}
await fetchIssues();
showModal = false;
}
async function deleteIssue(id: number) {
// function get public URL for image supabase
async function getPublicUrl(path: string): Promise<string> {
const { data } = supabase.storage.from("villabugis").getPublicUrl(path);
return data.publicUrl;
}
async function deleteIssue(id: string) {
if (confirm("Are you sure you want to delete this issue?")) {
const { error } = await supabase
.from("issues")
@@ -373,8 +423,6 @@
}
}
export let formErrors = writable<{ [key: string]: string }>({});
function validateForm(formData: FormData): boolean {
const errors: { [key: string]: string } = {};
const requiredFields = [
@@ -405,7 +453,7 @@
}
// insert id issue to project
async function moveIssueToProject(issueId: number) {
async function moveIssueToProject(issueId: string) {
// update move_issue field in the issue
const { error: updateError } = await supabase
.from("issues")
@@ -431,7 +479,7 @@
}
// insert id issue to purchase order
async function moveIssueToPurchaseOrder(issueId: number) {
async function moveIssueToPurchaseOrder(issueId: string) {
// update move_issue field in the issue
const { error: updateError } = await supabase
.from("issues")
@@ -457,19 +505,53 @@
</script>
<div>
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<div
class="p-6 bg-white shadow-md rounded-2xl mb-4 flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4"
>
<div>
<h2 class="text-lg font-semibold text-gray-800">Issue List</h2>
<p class="text-sm text-gray-600">
<h2 class="text-xl font-semibold text-gray-800">📝 Issue List</h2>
<p class="text-sm text-gray-500">
Manage and view all issues reported in the system.
</p>
</div>
<button
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
on:click={() => openModal()}
>
Add Issue
</button>
<div class="flex flex-col sm:flex-row sm:items-center gap-2">
<input
type="text"
placeholder="🔍 Search by name..."
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");
}}
/>
<select
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>
</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)}
>
🔄 Reset
</button>
<button
class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 text-sm"
on:click={() => openModal()}
>
Add Issue
</button>
</div>
</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">
@@ -494,7 +576,7 @@
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white">
{#each paginatedRows as row}
{#each allRows as row}
<tr class="hover:bg-gray-50 transition">
{#each columns as col}
{#if col.key === "name"}
@@ -566,6 +648,38 @@
{/if}
</td>
{:else if col.key === "issue_related_image"}
<td class="px-4 py-2">
{#if typeof row[col.key as keyof Issue] === "string" && row[col.key as keyof Issue]}
{#await getPublicUrl(row[col.key as keyof Issue] as string) then publicUrl}
<a
href={publicUrl}
target="_blank"
class="text-blue-600 hover:underline"
>View Picture</a
>
{:catch}
<span class="text-red-500"
>Error loading image</span
>
{/await}
{:else}
No Picture
{/if}
</td>
{:else if col.key === "reported_date"}
<td class="px-4 py-2">
{typeof row[col.key as keyof Issue] ===
"string" ||
typeof row[col.key as keyof Issue] ===
"number"
? new Date(
row[col.key as keyof Issue] as
| string
| number,
).toLocaleDateString("en-US")
: ""}
</td>
{:else if col.key === "need_approval"}
<td class="px-4 py-2">
{#if row[col.key as keyof Issue]}
@@ -716,6 +830,14 @@
alt="Preview"
class="mt-2 max-h-48 rounded border"
/>
{:else if newIssue.issue_related_image}
{#await getPublicUrl(newIssue.issue_related_image) then url}
<img
src={url}
alt="Preview"
class="mt-2 max-h-48 rounded border"
/>
{/await}
{/if}
</div>
{:else if col.key === "reported_date"}