Compare commits

..

No commits in common. "4183ec1e36969602ca3c9f88dbde721b375fd1e8" and "783a0f6dda088f7ecf44e2168ee2149beedc3a75" have entirely different histories.

5 changed files with 49 additions and 159 deletions

View File

@ -2,7 +2,6 @@
namespace App\Http\Controllers\Admin\Finances; namespace App\Http\Controllers\Admin\Finances;
use App\Enums\StudentStatus;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Finances\TuitionInvoiceRequest; use App\Http\Requests\Admin\Finances\TuitionInvoiceRequest;
use App\Http\Requests\PaginatedRequest; use App\Http\Requests\PaginatedRequest;
@ -29,7 +28,7 @@ public function index(PaginatedRequest $request): Response
...$request->validatedWithDefaults(), ...$request->validatedWithDefaults(),
academicTermId: $request->validated('academic_term_id'), academicTermId: $request->validated('academic_term_id'),
), ),
'students' => $this->studentService->getAllForSelect(status: StudentStatus::Active->value), 'students' => $this->studentService->getAllForSelect(),
'academicTerms' => $this->academicTermService->getAllForSelect(), 'academicTerms' => $this->academicTermService->getAllForSelect(),
'filters' => $request->only(['academic_term_id']), 'filters' => $request->only(['academic_term_id']),
]); ]);
@ -37,7 +36,7 @@ public function index(PaginatedRequest $request): Response
public function store(TuitionInvoiceRequest $request): RedirectResponse public function store(TuitionInvoiceRequest $request): RedirectResponse
{ {
$this->service->createMany($request->validated('student_ids'), $request->validated()); $this->service->create($request->validated());
Inertia::flash('toast', ['type' => 'success', 'message' => 'Tagihan berhasil ditambahkan.']); Inertia::flash('toast', ['type' => 'success', 'message' => 'Tagihan berhasil ditambahkan.']);

View File

@ -14,19 +14,11 @@ public function authorize(): bool
public function rules(): array public function rules(): array
{ {
$rules = [ return [
'student_id' => ['required', 'integer', Rule::exists('students', 'id')],
'academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')], 'academic_term_id' => ['required', 'integer', Rule::exists('academic_terms', 'id')],
'amount_due' => ['required', 'numeric', 'min:0'], 'amount_due' => ['required', 'numeric', 'min:0'],
'due_date' => ['required', 'date'], 'due_date' => ['nullable', 'date'],
]; ];
if ($this->isMethod('post')) {
$rules['student_ids'] = ['required', 'array', 'min:1'];
$rules['student_ids.*'] = ['integer', Rule::exists('students', 'id')];
} else {
$rules['student_id'] = ['required', 'integer', Rule::exists('students', 'id')];
}
return $rules;
} }
} }

View File

@ -4,7 +4,6 @@
use App\Models\TuitionInvoice; use App\Models\TuitionInvoice;
use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class TuitionInvoiceService class TuitionInvoiceService
{ {
@ -22,19 +21,15 @@ public function paginated(int $perPage = 25, string $search = '', ?int $academic
->paginate($perPage); ->paginate($perPage);
} }
public function createMany(array $studentIds, array $data): void public function create(array $data): TuitionInvoice
{ {
DB::transaction(function () use ($studentIds, $data) { return TuitionInvoice::create([
foreach ($studentIds as $studentId) { 'student_id' => $data['student_id'],
TuitionInvoice::create([
'student_id' => $studentId,
'academic_term_id' => $data['academic_term_id'], 'academic_term_id' => $data['academic_term_id'],
'amount_due' => $data['amount_due'], 'amount_due' => $data['amount_due'],
'due_date' => $data['due_date'], 'due_date' => $data['due_date'] ?? null,
]); ]);
} }
});
}
public function update(TuitionInvoice $invoice, array $data): TuitionInvoice public function update(TuitionInvoice $invoice, array $data): TuitionInvoice
{ {

View File

@ -12,7 +12,7 @@
class StudentService class StudentService
{ {
public function getAllForSelect(?string $status = null): Collection public function getAllForSelect(): Collection
{ {
return Student::select(['id', 'user_id', 'student_number', 'department_id']) return Student::select(['id', 'user_id', 'student_number', 'department_id'])
->with([ ->with([
@ -20,7 +20,6 @@ public function getAllForSelect(?string $status = null): Collection
'user.profile:id,user_id,full_name', 'user.profile:id,user_id,full_name',
'department:id,name', 'department:id,name',
]) ])
->when($status, fn ($q) => $q->where('status', $status))
->get(); ->get();
} }

View File

@ -13,18 +13,6 @@ import InputError from '@/components/input-error';
import { PageHeader } from '@/components/page-header'; import { PageHeader } from '@/components/page-header';
import { RupiahInput } from '@/components/rupiah-input'; import { RupiahInput } from '@/components/rupiah-input';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
useComboboxAnchor,
} from '@/components/ui/combobox';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { import {
Select, Select,
@ -221,15 +209,6 @@ function CreateForm({
academicTerms: AcademicTermOption[]; academicTerms: AcademicTermOption[];
}) { }) {
const [dueDate, setDueDate] = useState<Date | undefined>(); const [dueDate, setDueDate] = useState<Date | undefined>();
const [selectedStudents, setSelectedStudents] = useState<
TuitionInvoiceStudent[]
>([]);
const studentAnchor = useComboboxAnchor();
function reset() {
setDueDate(undefined);
setSelectedStudents([]);
}
return ( return (
<FormDialog <FormDialog
@ -240,84 +219,33 @@ function CreateForm({
resetOnSuccess resetOnSuccess
onSuccess={() => { onSuccess={() => {
onOpenChange(false); onOpenChange(false);
reset(); setDueDate(undefined);
}} }}
> >
{({ errors }) => ( {({ errors }) => (
<div className="grid gap-4"> <div className="grid gap-4">
<div className="grid gap-2"> <div className="grid gap-2">
<div className="flex items-center justify-between">
<Label> <Label>
Mahasiswa{' '} Mahasiswa{' '}
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
</Label> </Label>
<button <input type="hidden" name="student_id" />
type="button" <Select name="student_id">
className="text-xs text-primary underline underline-offset-4 hover:text-primary/80" <SelectTrigger className="w-full">
onClick={() => <SelectValue placeholder="Pilih mahasiswa" />
setSelectedStudents( </SelectTrigger>
selectedStudents.length === <SelectContent>
students.length
? []
: students,
)
}
>
{selectedStudents.length === students.length
? 'Batalkan Semua'
: 'Pilih Semua'}
</button>
</div>
{selectedStudents.map((student) => (
<input
key={student.id}
type="hidden"
name="student_ids[]"
value={student.id}
/>
))}
<Combobox
items={students}
multiple
value={selectedStudents}
onValueChange={setSelectedStudents}
itemToStringLabel={studentLabel}
isItemEqualToValue={(a, b) => a.id === b.id}
>
<ComboboxChips ref={studentAnchor}>
{selectedStudents.map((student) => (
<ComboboxChip
key={student.id}
aria-label={studentLabel(student)}
>
{studentLabel(student)}
</ComboboxChip>
))}
<ComboboxChipsInput
placeholder={
selectedStudents.length === 0
? 'Pilih mahasiswa (aktif)'
: ''
}
/>
</ComboboxChips>
<ComboboxContent anchor={studentAnchor}>
<ComboboxEmpty>
Mahasiswa tidak ditemukan.
</ComboboxEmpty>
<ComboboxList>
{students.map((student) => ( {students.map((student) => (
<ComboboxItem <SelectItem
key={student.id} key={student.id}
value={student} value={String(student.id)}
> >
{studentLabel(student)} {studentLabel(student)}
</ComboboxItem> </SelectItem>
))} ))}
</ComboboxList> </SelectContent>
</ComboboxContent> </Select>
</Combobox> <InputError message={errors.student_id} />
<InputError message={errors.student_ids} />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label> <Label>
@ -356,10 +284,7 @@ function CreateForm({
<InputError message={errors.amount_due} /> <InputError message={errors.amount_due} />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label> <Label>Jatuh Tempo</Label>
Jatuh Tempo{' '}
<span className="text-destructive">*</span>
</Label>
<input <input
type="hidden" type="hidden"
name="due_date" name="due_date"
@ -394,9 +319,6 @@ function EditForm({
const [dueDate, setDueDate] = useState<Date | undefined>( const [dueDate, setDueDate] = useState<Date | undefined>(
editing?.due_date ? new Date(editing.due_date) : undefined, editing?.due_date ? new Date(editing.due_date) : undefined,
); );
const [student, setStudent] = useState<TuitionInvoiceStudent | null>(
students.find((s) => s.id === editing?.student_id) ?? null,
);
return ( return (
<FormDialog <FormDialog
@ -415,38 +337,24 @@ function EditForm({
Mahasiswa{' '} Mahasiswa{' '}
<span className="text-destructive">*</span> <span className="text-destructive">*</span>
</Label> </Label>
<input <Select
type="hidden"
name="student_id" name="student_id"
value={student?.id ?? ''} defaultValue={String(editing.student_id)}
/>
<Combobox
items={students}
value={student}
onValueChange={setStudent}
itemToStringLabel={studentLabel}
isItemEqualToValue={(a, b) => a.id === b.id}
> >
<ComboboxInput <SelectTrigger className="w-full">
placeholder="Pilih mahasiswa (aktif)" <SelectValue placeholder="Pilih mahasiswa" />
className="w-full" </SelectTrigger>
/> <SelectContent>
<ComboboxContent> {students.map((student) => (
<ComboboxEmpty> <SelectItem
Mahasiswa tidak ditemukan. key={student.id}
</ComboboxEmpty> value={String(student.id)}
<ComboboxList>
{students.map((option) => (
<ComboboxItem
key={option.id}
value={option}
> >
{studentLabel(option)} {studentLabel(student)}
</ComboboxItem> </SelectItem>
))} ))}
</ComboboxList> </SelectContent>
</ComboboxContent> </Select>
</Combobox>
<InputError message={errors.student_id} /> <InputError message={errors.student_id} />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
@ -488,10 +396,7 @@ function EditForm({
<InputError message={errors.amount_due} /> <InputError message={errors.amount_due} />
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">
<Label> <Label>Jatuh Tempo</Label>
Jatuh Tempo{' '}
<span className="text-destructive">*</span>
</Label>
<input <input
type="hidden" type="hidden"
name="due_date" name="due_date"