11
Import dan Export Ticket
Ada tiga jalur Excel di aplikasi ini: impor ticket dari berkas .xlsx/.xls, ekspor ticket ke .xlsx, dan unduhan template impor. Ketiganya memakai facade Excel dari maatwebsite/excel dan menutup prosesnya dengan satu Notification Filament. Aturan status, prioritas, dan epic yang dipakai saat impor dijelaskan di Manajemen Ticket; tombol ekspor sendiri dipasang di Papan Kanban dan Timeline.
Peta kelas dan titik masuk
ImportTicketsAction::make() mengembalikan Action bernama import_tickets dengan label "Import from Excel", ikon heroicon-m-arrow-up-tray, dan warna success app/Filament/Actions/ImportTicketsAction.php:21-261. Di repository ini tidak ada pemanggil ImportTicketsAction::make(); kelasnya hanya muncul sebagai baris use di resource project app/Filament/Resources/Projects/ProjectResource.php:282.
Aksi impor yang benar-benar terpasang adalah versi inline di TicketsRelationManager dengan nama aksi dan struktur form yang sama, tetapi project diambil dari owner record, bukan dari Select app/Filament/Resources/Projects/RelationManagers/TicketsRelationManager.php:242-2763.
ExportTicketsAction::make() dipasang pada header actions ProjectBoard dan hanya tampil bila sebuah project sudah dipilih dan user memegang role super_admin app/Filament/Pages/ProjectBoard.php:387-3884.
DownloadTicketTemplateAction berdiri sendiri dan tidak dipanggil di tempat lain. Unduhan template tersedia sebagai aksi bersarang download_template di dalam form impor app/Filament/Actions/ImportTicketsAction.php:54-795. Kelas mandiri itu memilih project lewat Select sendiri app/Filament/Actions/DownloadTicketTemplateAction.php:22-386, lalu menyusun nama berkas dari nama project dan now()->format('Y-m-d') sebelum memanggil Excel::download app/Filament/Actions/DownloadTicketTemplateAction.php:40-537.
Batas form impor
Tabel berikut merangkum batas yang dipasang pada form impor app/Filament/Actions/ImportTicketsAction.php:31-528 dan pada unggahan berkasnya app/Filament/Actions/ImportTicketsAction.php:81-909.
| Bagian form | Batas |
|---|---|
Select project_id | hanya project yang punya member dengan user_id milik user aktif, super_admin melihat semua project, required, searchable, live |
| Pergantian project | excel_file direset ke null lewat afterStateUpdated |
FileUpload excel_file | tipe application/vnd.openxmlformats-officedocument.spreadsheetml.sheet dan application/vnd.ms-excel, maxSize(5120) alias 5 MB, disk('local'), direktori temp-imports, visibility('private') |
Alur impor
Handler aksi mengambil Project::findOrFail($data['project_id']), mengubah nama berkas unggahan menjadi path absolut lewat Storage::disk('local')->path(), lalu menjalankan Excel::import(new TicketsImport($project), $filePath) app/Filament/Actions/ImportTicketsAction.php:93-10610. Setelah impor selesai, handler membaca getImportedCount(), errors(), dan failures(), menghapus berkas sementara dari disk local, baru kemudian mengirim notifikasi. getImportedCount() hanya mengembalikan penghitung $importedCount yang dinaikkan per baris sukses app/Imports/TicketsImport.php:191-19411.
sequenceDiagram
autonumber
participant U as Admin
participant A as ImportTicketsAction
participant X as Excel facade
participant I as TicketsImport
participant DB as tickets table
participant N as Notification
U->>A: pilih project_id lalu unggah excel_file
U->>A: klik unduh template
A->>X: Excel::download TicketTemplateExport
X-->>U: berkas xlsx tiga sheet
U->>A: submit form impor
A->>X: Excel::import TicketsImport dengan path lokal
loop setiap baris berheading
I->>I: model row, lewati baris tanpa title
I->>DB: Ticket::create
I->>DB: assignees sync email member
I->>I: importedCount bertambah
end
A->>I: getImportedCount lalu errors lalu failures
A->>A: hapus berkas sementara dari disk local
alt importedCount lebih besar 0
A->>N: Import Completed success
else tidak ada baris masuk
A->>N: Import Failed warning persistent
end
Note over A,N: Exception memicu Import Error danger persistent
%% fig: Siklus satu request impor ticket
%% desc: Form mengunggah berkas, TicketsImport membuat ticket per baris, lalu handler mengirim notifikasi sukses atau gagal setelah berkas sementara dihapus.TicketsImport mengimplementasikan ToModel, WithHeadingRow, WithValidation, SkipsOnError, dan SkipsOnFailure, serta menyimpan Project dan penghitung impor app/Imports/TicketsImport.php:25-3512. Karena WithHeadingRow aktif, kunci setiap baris diambil dari heading yang di-slug, misalnya Due Date YYYY-MM-DD menjadi due_date_yyyy_mm_dd.
model() melewati baris tanpa title dan baris contoh template, lalu mencari TicketStatus pada project; bila status tidak ditemukan, baris dikembalikan null sehingga dilewati app/Imports/TicketsImport.php:37-5813. priority dicari di tabel ticket_priorities, sedangkan epic dicari pada relasi epics() project; keduanya opsional app/Imports/TicketsImport.php:60-7014.
Tanggal due_date_yyyy_mm_dd menerima serial numerik Excel maupun enam format teks (Y-m-d, m/d/Y, d/m/Y, Y/m/d, d-m-Y, m-d-Y), lalu jatuh ke Carbon::parse() bila semua gagal app/Imports/TicketsImport.php:72-10215. start_date_yyyy_mm_dd memakai logika yang sama untuk kolom start_date app/Imports/TicketsImport.php:104-13016.
Ticket dibuat lewat Ticket::create() dengan project_id, ticket_status_id, priority_id, epic_id, start_date, due_date, dan created_by dari auth()->id() app/Imports/TicketsImport.php:132-16617. Email pada kolom assignee difilter: hanya email valid yang benar-benar member project yang masuk assignees()->sync($userIds).
Template impor dan pemetaan kolom
Template adalah satu workbook dengan tiga sheet, yaitu Ticket Template, Project Members, dan Reference Data app/Exports/TicketTemplateExport.php:27-3418.
graph TD
T["TicketTemplateExport"] --> S1["TicketTemplateSheet<br/>sheet Ticket Template"]
T --> S2["ProjectMembersSheet<br/>sheet Project Members"]
T --> S3["ReferenceDataSheet<br/>sheet Reference Data"]
S1 --> H1["8 heading tetap<br/>Title sampai Due Date YYYY-MM-DD"]
S1 --> H2["1 baris contoh<br/>dibuang importer"]
S2 --> M1["Name, Email, Role<br/>dari members project"]
S3 --> R1["status project<br/>seluruh ticket_priorities<br/>epic project"]
style T fill:#f59e0b,color:#000
style S2 fill:#8b5cf6,color:#fff
style S3 fill:#8b5cf6,color:#fff
style H2 fill:#dc2626,color:#fff
style M1 fill:#2d7d46,color:#fff
%% fig: Struktur workbook template impor
%% desc: Satu TicketTemplateExport merangkai tiga sheet, dan hanya sheet Ticket Template yang dibaca baris per baris oleh TicketsImport.Sheet Ticket Template memakai delapan heading tetap dari Title sampai Due Date YYYY-MM-DD app/Exports/TicketTemplateExport.php:51-6319 dan menyertakan satu baris contoh yang justru dibuang oleh importer app/Exports/TicketTemplateExport.php:65-8120. Sheet Project Members mengisi Name, Email, dan Role dari $project->members() agar email assignee bisa dicocokkan app/Exports/TicketTemplateExport.php:122-14521. Sheet Reference Data menampilkan tiga kolom: status milik project, seluruh ticket_priorities, dan epic milik project app/Exports/TicketTemplateExport.php:186-21322.
Aturan validasi dikumpulkan pada rules() app/Imports/TicketsImport.php:168-18923, dan tabel berikut memasangkannya dengan heading template.
| Heading template | Kunci baris | Wajib | Perilaku importer |
|---|---|---|---|
| Title | title | ya | required, string, max:255, nilai disimpan ke name ticket |
| Description | description | tidak | nullable, string |
| Status | status | ya | Rule::exists('ticket_statuses', 'name') dibatasi project_id |
| Priority | priority | tidak | exists:ticket_priorities,name |
| Epic | epic | tidak | Rule::exists('epics', 'name') dibatasi project_id |
| Assignees Comma Separated Emails | assignees_comma_separated_emails | tidak | hanya email valid dan member project yang di-sync |
| Start Date YYYY-MM-DD | start_date_yyyy_mm_dd | tidak | nullable, date, parser tanggal fleksibel |
| Due Date YYYY-MM-DD | due_date_yyyy_mm_dd | tidak | nullable, date, parser tanggal fleksibel |
Notifikasi hasil impor
Berkas sementara selalu dihapus lebih dulu, baik pada jalur sukses maupun pada blok catch. Tiga cabang notifikasi ada di handler aksi app/Filament/Actions/ImportTicketsAction.php:108-11924, app/Filament/Actions/ImportTicketsAction.php:120-15725, dan app/Filament/Actions/ImportTicketsAction.php:159-18126.
| Kondisi | Judul | Tipe | Isi |
|---|---|---|---|
importedCount lebih dari 0 | Import Completed | success | "Successfully imported {N} ticket(s) to project '{nama}'." ditambah " Some rows had errors and were skipped." bila ada errors() atau failures() |
0 baris masuk, ada failures() | Import Failed | warning, persistent() | "Validation errors found in N row(s):" lalu "Row {baris}: {pesan}" |
0 baris masuk, ada errors() | Import Failed | warning, persistent() | "Processing errors:" lalu tiap pesan galat |
| 0 baris masuk tanpa keduanya | Import Failed | warning, persistent() | lima butir saran: format template, kolom Title dan Status, nama status, YYYY-MM-DD, email assignee |
| Exception | Import Error | danger, persistent() | $e->getMessage() plus saran sesuai kata kunci file, database, atau SQL |
⚠️ Baris tanpa
titledan baris contoh template dikembalikannulltanpa pesan apa pun app/Imports/TicketsImport.php:39-4827. Untuk kasus itufailures()tetap kosong, sehingga berkas yang hanya berisi header berakhir pada notifikasiImport Failedberisi lima saran umum app/Filament/Actions/ImportTicketsAction.php:139-14928, bukan daftar baris bermasalah.
Versi inline di TicketsRelationManager memakai teks berbeda: blok **Validation Errors:** per baris dan blok **Possible causes:** bila tidak ada catatan error app/Filament/Resources/Projects/RelationManagers/TicketsRelationManager.php:304-34329.
Alur ekspor
ExportTicketsAction menyusun form CheckboxList columns dengan sepuluh opsi, default enam kolom (uuid, name, status, assignee, due_date, created_at), required(), dan minItems(1) app/Filament/Actions/ExportTicketsAction.php:17-4530. Aksi itu tidak mengekspor sendiri: ia memanggil $livewire->exportTickets($data['columns'] ?? []) app/Filament/Actions/ExportTicketsAction.php:46-4831.
graph LR
A["ExportTicketsAction<br/>CheckboxList columns"] --> B["ProjectBoard::exportTickets"]
B --> C{"kolom kosong atau tickets kosong?"}
C -- ya --> E["Notification Export Failed<br/>warning atau danger"]
C -- tidak --> D["TicketsExport<br/>map dan headings"]
D --> F["Excel::store ke exports/<br/>disk public"]
F --> G["asset storage/exports lalu fetch<br/>di browser"]
G --> H["Notification Export Successful"]
style A fill:#f59e0b,color:#000
style F fill:#336791,color:#fff
style G fill:#8b5cf6,color:#fff
style H fill:#2d7d46,color:#fff
style E fill:#dc2626,color:#fff
%% fig: Alur data dari form ekspor ke unduhan
%% desc: Aksi hanya meneruskan kolom terpilih, ProjectBoard menyusun koleksi ticket, TicketsExport memetakan sel, lalu berkas diunduh lewat fetch dari disk public.exportTickets() menolak daftar kolom kosong, lalu mengambil ticket project terpilih bersama relasi assignees, status, project, dan epic berurutan created_at menurun; bila tidak ada project terpilih, ticket dikumpulkan dari ticketStatuses app/Filament/Pages/ProjectBoard.php:478-51632. Daftar yang kosong memicu notifikasi Export Failed bertipe warning dan method berhenti tanpa menulis berkas.
Berkas disimpan lewat Excel::store($export, 'exports/'.$fileName, 'public') sehingga unduhan memakai URL asset('storage/exports/'.$fileName) app/Filament/Pages/ProjectBoard.php:518-52333. Proses unduh berjalan di browser: fetch() mengambil blob, a.click() memicu unduhan, lalu URL.revokeObjectURL() membersihkan URL sementara.
⚠️ Karena berkas ditulis ke disk
publicdi direktoriexports, tombol ekspor bergantung pada symlinkstoragemenujustorage/app/public. Tanpa symlink itu, URL hasilasset()tidak menunjuk berkas yang sama.
Setelah berkas tersimpan, handler mengirim Export Successful app/Filament/Pages/ProjectBoard.php:540-54434; seluruh blok dibungkus try/catch yang mengubah exception menjadi Export Failed bertipe danger berisi $e->getMessage() app/Filament/Pages/ProjectBoard.php:546-55235.
Kolom ekspor dan isi sel
TicketsExport mendefinisikan sebelas kolom di $availableColumns, termasuk start_date app/Exports/TicketsExport.php:19-3136. Form ekspor hanya menawarkan sepuluh di antaranya, sehingga start_date tidak dapat dipilih dari CheckboxList.
Heading diambil dari $availableColumns sesuai urutan kolom terpilih app/Exports/TicketsExport.php:44-5337, sedangkan isi sel dihasilkan map() app/Exports/TicketsExport.php:55-10038.
| Kunci | Heading | Nilai sel |
|---|---|---|
uuid | Ticket ID | $ticket->uuid |
name | Title | $ticket->name |
description | Description | strip_tags($ticket->description ?? '') |
status | Status | $ticket->status?->name atau No Status |
assignee | Assignee | nama assignee di-implode(', ') |
project | Project | nama project atau No Project |
epic | Epic | nama epic atau No Epic |
start_date | Start Date | format('Y-m-d') atau string kosong |
due_date | Due Date | format('Y-m-d') atau string kosong |
created_at | Created At | format('Y-m-d H:i:s') |
updated_at | Updated At | format('Y-m-d H:i:s') |
Baris pertama lembar diberi latar FF366092 dengan teks putih tebal app/Exports/TicketsExport.php:103-12539.