penambahan fitur upload
This commit is contained in:
@@ -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"}
|
||||
|
||||
Reference in New Issue
Block a user