Merge pull request 'feat: enhance Tuition Invoice management with academic term formatting and validation' (#66) from feat/enhance-tuition-invoice into dev
Reviewed-on: #66
This commit is contained in:
commit
d0793b739a
@ -24,12 +24,20 @@ public function __construct(
|
||||
|
||||
public function index(PaginatedRequest $request): Response
|
||||
{
|
||||
$students = $this->studentService->getAllForSelect(status: StudentStatus::Active->value);
|
||||
|
||||
$invoicedTermIdsByStudent = $this->service->getInvoicedTermIdsByStudent($students->pluck('id')->all());
|
||||
|
||||
$students->each(function ($student) use ($invoicedTermIdsByStudent) {
|
||||
$student->invoiced_term_ids = $invoicedTermIdsByStudent->get($student->id, []);
|
||||
});
|
||||
|
||||
return Inertia::render('admin/finances/tuition-invoices/index', [
|
||||
'invoices' => $this->service->paginated(
|
||||
...$request->validatedWithDefaults(),
|
||||
academicTermId: $request->validated('academic_term_id'),
|
||||
),
|
||||
'students' => $this->studentService->getAllForSelect(status: StudentStatus::Active->value),
|
||||
'students' => $students,
|
||||
'academicTerms' => $this->academicTermService->getAllForSelect(),
|
||||
'filters' => $request->only(['academic_term_id']),
|
||||
]);
|
||||
|
||||
@ -22,11 +22,33 @@ public function rules(): array
|
||||
|
||||
if ($this->isMethod('post')) {
|
||||
$rules['student_ids'] = ['required', 'array', 'min:1'];
|
||||
$rules['student_ids.*'] = ['integer', Rule::exists('students', 'id')];
|
||||
$rules['student_ids.*'] = [
|
||||
'integer',
|
||||
Rule::exists('students', 'id'),
|
||||
Rule::unique('tuition_invoices', 'student_id')
|
||||
->where('academic_term_id', $this->input('academic_term_id')),
|
||||
];
|
||||
} else {
|
||||
$rules['student_id'] = ['required', 'integer', Rule::exists('students', 'id')];
|
||||
$invoice = $this->route('tuition_invoice');
|
||||
|
||||
$rules['student_id'] = [
|
||||
'required',
|
||||
'integer',
|
||||
Rule::exists('students', 'id'),
|
||||
Rule::unique('tuition_invoices', 'student_id')
|
||||
->where('academic_term_id', $this->input('academic_term_id'))
|
||||
->ignore($invoice?->id),
|
||||
];
|
||||
}
|
||||
|
||||
return $rules;
|
||||
}
|
||||
|
||||
public function messages(): array
|
||||
{
|
||||
return [
|
||||
'student_ids.*.unique' => 'Salah satu mahasiswa yang dipilih sudah memiliki tagihan untuk periode akademik tersebut.',
|
||||
'student_id.unique' => 'Mahasiswa ini sudah memiliki tagihan untuk periode akademik tersebut.',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,10 +4,24 @@
|
||||
|
||||
use App\Models\TuitionInvoice;
|
||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class TuitionInvoiceService
|
||||
{
|
||||
/**
|
||||
* @param array<int> $studentIds
|
||||
* @return Collection<int, array<int>> daftar academic_term_id per student_id yang sudah punya tagihan
|
||||
*/
|
||||
public function getInvoicedTermIdsByStudent(array $studentIds): Collection
|
||||
{
|
||||
return TuitionInvoice::query()
|
||||
->whereIn('student_id', $studentIds)
|
||||
->get(['student_id', 'academic_term_id'])
|
||||
->groupBy('student_id')
|
||||
->map(fn ($rows) => $rows->pluck('academic_term_id')->values()->all());
|
||||
}
|
||||
|
||||
public function paginated(int $perPage = 25, string $search = '', ?int $academicTermId = null): LengthAwarePaginator
|
||||
{
|
||||
return TuitionInvoice::query()
|
||||
|
||||
@ -110,7 +110,7 @@ function ComboboxContent({
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
anchor={anchor}
|
||||
className="isolate z-50"
|
||||
className="isolate z-50 pointer-events-auto"
|
||||
>
|
||||
<ComboboxPrimitive.Popup
|
||||
data-slot="combobox-content"
|
||||
@ -126,10 +126,29 @@ function ComboboxContent({
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
|
||||
function ComboboxList({
|
||||
className,
|
||||
ref,
|
||||
...props
|
||||
}: ComboboxPrimitive.List.Props & { ref?: React.Ref<HTMLDivElement> }) {
|
||||
const listRef = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
return (
|
||||
<ComboboxPrimitive.List
|
||||
ref={(node) => {
|
||||
listRef.current = node
|
||||
if (typeof ref === "function") ref(node)
|
||||
else if (ref) ref.current = node
|
||||
}}
|
||||
data-slot="combobox-list"
|
||||
onWheel={(event) => {
|
||||
// Radix Dialog's scroll lock (react-remove-scroll) always cancels
|
||||
// wheel scroll on elements outside its own lock/shards, even if
|
||||
// they're independently scrollable. Since this popup is portaled by
|
||||
// Base UI (a different library not registered with Radix), scroll
|
||||
// it manually instead of relying on the browser's default action.
|
||||
listRef.current?.scrollBy({ top: event.deltaY })
|
||||
}}
|
||||
className={cn(
|
||||
"max-h-[min(calc(--spacing(96)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto p-1 data-empty:p-0",
|
||||
className
|
||||
|
||||
@ -5,6 +5,7 @@ import { RowActions } from '@/components/row-actions';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { formatRupiah } from '@/lib/currency';
|
||||
import { index as paymentsIndex } from '@/routes/admin/finances/tuition-invoices/payments';
|
||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
||||
import type { TuitionInvoice } from '@/types/tuition-invoice';
|
||||
|
||||
export type { TuitionInvoice } from '@/types/tuition-invoice';
|
||||
@ -42,7 +43,11 @@ export function createTuitionInvoiceColumns(
|
||||
{
|
||||
accessorKey: 'academic_term.name',
|
||||
header: () => <span>Periode</span>,
|
||||
cell: ({ row }) => row.original.academic_term?.name ?? '-',
|
||||
cell: ({ row }) => {
|
||||
const term = row.original.academic_term;
|
||||
|
||||
return term ? formatAcademicTermLabel(term) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'amount_due',
|
||||
|
||||
@ -40,6 +40,7 @@ import {
|
||||
store,
|
||||
update,
|
||||
} from '@/routes/admin/finances/tuition-invoices';
|
||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
||||
import type {
|
||||
TuitionInvoice,
|
||||
TuitionInvoiceStudent,
|
||||
@ -85,7 +86,7 @@ export default function TuitionInvoiceIndex({
|
||||
label: 'Periode Akademik',
|
||||
options: academicTerms.map((term) => ({
|
||||
value: String(term.id),
|
||||
label: term.name,
|
||||
label: formatAcademicTermLabel(term),
|
||||
})),
|
||||
},
|
||||
];
|
||||
@ -220,13 +221,22 @@ function CreateForm({
|
||||
students: TuitionInvoiceStudent[];
|
||||
academicTerms: AcademicTermOption[];
|
||||
}) {
|
||||
const [academicTermId, setAcademicTermId] = useState('');
|
||||
const [dueDate, setDueDate] = useState<Date | undefined>();
|
||||
const [selectedStudents, setSelectedStudents] = useState<
|
||||
TuitionInvoiceStudent[]
|
||||
>([]);
|
||||
const studentAnchor = useComboboxAnchor();
|
||||
|
||||
const availableStudents = academicTermId
|
||||
? students.filter(
|
||||
(student) =>
|
||||
!student.invoiced_term_ids?.includes(Number(academicTermId)),
|
||||
)
|
||||
: [];
|
||||
|
||||
function reset() {
|
||||
setAcademicTermId('');
|
||||
setDueDate(undefined);
|
||||
setSelectedStudents([]);
|
||||
}
|
||||
@ -245,6 +255,39 @@ function CreateForm({
|
||||
>
|
||||
{({ errors }) => (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Periode Akademik{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<input
|
||||
type="hidden"
|
||||
name="academic_term_id"
|
||||
value={academicTermId}
|
||||
/>
|
||||
<Select
|
||||
value={academicTermId}
|
||||
onValueChange={(value) => {
|
||||
setAcademicTermId(value);
|
||||
setSelectedStudents([]);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih periode akademik" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{academicTerms.map((term) => (
|
||||
<SelectItem
|
||||
key={term.id}
|
||||
value={String(term.id)}
|
||||
>
|
||||
{formatAcademicTermLabel(term)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={errors.academic_term_id} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>
|
||||
@ -253,17 +296,20 @@ function CreateForm({
|
||||
</Label>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-primary underline underline-offset-4 hover:text-primary/80"
|
||||
disabled={!academicTermId}
|
||||
className="text-xs text-primary underline underline-offset-4 hover:text-primary/80 disabled:pointer-events-none disabled:opacity-50"
|
||||
onClick={() =>
|
||||
setSelectedStudents(
|
||||
selectedStudents.length ===
|
||||
students.length
|
||||
availableStudents.length
|
||||
? []
|
||||
: students,
|
||||
: availableStudents,
|
||||
)
|
||||
}
|
||||
>
|
||||
{selectedStudents.length === students.length
|
||||
{selectedStudents.length ===
|
||||
availableStudents.length &&
|
||||
availableStudents.length > 0
|
||||
? 'Batalkan Semua'
|
||||
: 'Pilih Semua'}
|
||||
</button>
|
||||
@ -277,8 +323,9 @@ function CreateForm({
|
||||
/>
|
||||
))}
|
||||
<Combobox
|
||||
items={students}
|
||||
items={availableStudents}
|
||||
multiple
|
||||
disabled={!academicTermId}
|
||||
value={selectedStudents}
|
||||
onValueChange={setSelectedStudents}
|
||||
itemToStringLabel={studentLabel}
|
||||
@ -294,10 +341,13 @@ function CreateForm({
|
||||
</ComboboxChip>
|
||||
))}
|
||||
<ComboboxChipsInput
|
||||
disabled={!academicTermId}
|
||||
placeholder={
|
||||
selectedStudents.length === 0
|
||||
? 'Pilih mahasiswa (aktif)'
|
||||
: ''
|
||||
selectedStudents.length > 0
|
||||
? ''
|
||||
: academicTermId
|
||||
? 'Pilih mahasiswa (aktif)'
|
||||
: 'Pilih periode akademik terlebih dahulu'
|
||||
}
|
||||
/>
|
||||
</ComboboxChips>
|
||||
@ -306,42 +356,19 @@ function CreateForm({
|
||||
Mahasiswa tidak ditemukan.
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{students.map((student) => (
|
||||
{(student: TuitionInvoiceStudent) => (
|
||||
<ComboboxItem
|
||||
key={student.id}
|
||||
value={student}
|
||||
>
|
||||
{studentLabel(student)}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
<InputError message={errors.student_ids} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Periode Akademik{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<input type="hidden" name="academic_term_id" />
|
||||
<Select name="academic_term_id">
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih periode akademik" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{academicTerms.map((term) => (
|
||||
<SelectItem
|
||||
key={term.id}
|
||||
value={String(term.id)}
|
||||
>
|
||||
{term.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={errors.academic_term_id} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="amount_due">
|
||||
Jumlah Tagihan{' '}
|
||||
@ -410,6 +437,31 @@ function EditForm({
|
||||
{({ errors }) =>
|
||||
editing && (
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Periode Akademik{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
name="academic_term_id"
|
||||
defaultValue={String(editing.academic_term_id)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih periode akademik" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{academicTerms.map((term) => (
|
||||
<SelectItem
|
||||
key={term.id}
|
||||
value={String(term.id)}
|
||||
>
|
||||
{formatAcademicTermLabel(term)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={errors.academic_term_id} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Mahasiswa{' '}
|
||||
@ -436,44 +488,19 @@ function EditForm({
|
||||
Mahasiswa tidak ditemukan.
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{students.map((option) => (
|
||||
{(option: TuitionInvoiceStudent) => (
|
||||
<ComboboxItem
|
||||
key={option.id}
|
||||
value={option}
|
||||
>
|
||||
{studentLabel(option)}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
<InputError message={errors.student_id} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label>
|
||||
Periode Akademik{' '}
|
||||
<span className="text-destructive">*</span>
|
||||
</Label>
|
||||
<Select
|
||||
name="academic_term_id"
|
||||
defaultValue={String(editing.academic_term_id)}
|
||||
>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Pilih periode akademik" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{academicTerms.map((term) => (
|
||||
<SelectItem
|
||||
key={term.id}
|
||||
value={String(term.id)}
|
||||
>
|
||||
{term.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<InputError message={errors.academic_term_id} />
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="edit-amount_due">
|
||||
Jumlah Tagihan{' '}
|
||||
|
||||
@ -32,6 +32,7 @@ import {
|
||||
store,
|
||||
update,
|
||||
} from '@/routes/admin/finances/tuition-invoices/payments';
|
||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
||||
import type { TuitionInvoice } from '@/types/tuition-invoice';
|
||||
import type { TuitionPayment } from '@/types/tuition-payment';
|
||||
import { PaymentStatusLabels, PaymentStatuses } from '@/types/tuition-payment';
|
||||
@ -195,7 +196,12 @@ export default function TuitionPaymentIndex({ invoice, payments }: Props) {
|
||||
NIM: {invoice.student?.student_number} ·{' '}
|
||||
{invoice.student?.department?.name ?? '-'}
|
||||
</p>
|
||||
<p>Periode: {invoice.academic_term?.name}</p>
|
||||
<p>
|
||||
Periode:{' '}
|
||||
{invoice.academic_term
|
||||
? formatAcademicTermLabel(invoice.academic_term)
|
||||
: '-'}
|
||||
</p>
|
||||
<p>
|
||||
Jumlah Tagihan: {formatRupiah(invoice.amount_due)}
|
||||
</p>
|
||||
|
||||
@ -3,6 +3,7 @@ import { Pencil, Trash2, Users } from 'lucide-react';
|
||||
import { RowActions } from '@/components/row-actions';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { index as enrollmentsIndex } from '@/routes/admin/manage/course-classes/enrollments';
|
||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
||||
import { ClassMethodLabels } from '@/types/course-class';
|
||||
import type { CourseClass } from '@/types/course-class';
|
||||
|
||||
@ -37,7 +38,11 @@ export function createCourseClassColumns(
|
||||
{
|
||||
accessorKey: 'academic_term.name',
|
||||
header: () => <span>Periode</span>,
|
||||
cell: ({ row }) => row.original.academic_term?.name ?? '-',
|
||||
cell: ({ row }) => {
|
||||
const term = row.original.academic_term;
|
||||
|
||||
return term ? formatAcademicTermLabel(term) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'method',
|
||||
|
||||
@ -18,6 +18,7 @@ import {
|
||||
destroy,
|
||||
store,
|
||||
} from '@/routes/admin/manage/course-classes/enrollments';
|
||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
||||
import type {
|
||||
ClassEnrollment,
|
||||
EnrollmentStudent,
|
||||
@ -131,7 +132,14 @@ export default function ClassEnrollmentIndex({
|
||||
{courseClass.lecturer?.user?.profile?.full_name ??
|
||||
'-'}
|
||||
</p>
|
||||
<p>Periode: {courseClass.academic_term?.name}</p>
|
||||
<p>
|
||||
Periode:{' '}
|
||||
{courseClass.academic_term
|
||||
? formatAcademicTermLabel(
|
||||
courseClass.academic_term,
|
||||
)
|
||||
: '-'}
|
||||
</p>
|
||||
<p>
|
||||
Metode:{' '}
|
||||
{courseClass.method
|
||||
|
||||
@ -33,6 +33,7 @@ import {
|
||||
store,
|
||||
update,
|
||||
} from '@/routes/admin/manage/course-classes';
|
||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
||||
import { ClassMethodLabels, ClassMethods } from '@/types/course-class';
|
||||
import type { CourseClass } from '@/types/course-class';
|
||||
import { createCourseClassColumns } from './columns';
|
||||
@ -82,7 +83,7 @@ export default function CourseClassIndex({
|
||||
label: 'Periode Akademik',
|
||||
options: academicTerms.map((term) => ({
|
||||
value: String(term.id),
|
||||
label: term.name,
|
||||
label: formatAcademicTermLabel(term),
|
||||
})),
|
||||
},
|
||||
{
|
||||
@ -292,7 +293,7 @@ function CreateForm({
|
||||
key={term.id}
|
||||
value={String(term.id)}
|
||||
>
|
||||
{term.name}
|
||||
{formatAcademicTermLabel(term)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@ -366,7 +367,7 @@ function CreateForm({
|
||||
Dosen tidak ditemukan.
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{availableLecturers.map((lect) => (
|
||||
{(lect: Lecturer) => (
|
||||
<ComboboxItem
|
||||
key={lect.id}
|
||||
value={lect}
|
||||
@ -375,7 +376,7 @@ function CreateForm({
|
||||
'N/A'}{' '}
|
||||
- {lect.lecturer_number}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
@ -517,7 +518,7 @@ function EditForm({
|
||||
Dosen tidak ditemukan.
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{availableLecturers.map((lect) => (
|
||||
{(lect: Lecturer) => (
|
||||
<ComboboxItem
|
||||
key={lect.id}
|
||||
value={lect}
|
||||
@ -526,7 +527,7 @@ function EditForm({
|
||||
?.full_name ?? 'N/A'}{' '}
|
||||
- {lect.lecturer_number}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
@ -550,7 +551,7 @@ function EditForm({
|
||||
key={term.id}
|
||||
value={String(term.id)}
|
||||
>
|
||||
{term.name}
|
||||
{formatAcademicTermLabel(term)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@ -3,6 +3,7 @@ import { format } from 'date-fns';
|
||||
import { Pencil, Trash2 } from 'lucide-react';
|
||||
import { RowActions } from '@/components/row-actions';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
||||
import type {
|
||||
CourseRegistration,
|
||||
RegistrationStatus,
|
||||
@ -62,7 +63,11 @@ export function createCourseRegistrationColumns(
|
||||
{
|
||||
accessorKey: 'academic_term.name',
|
||||
header: () => <span>Periode</span>,
|
||||
cell: ({ row }) => row.original.academic_term?.name ?? '-',
|
||||
cell: ({ row }) => {
|
||||
const term = row.original.academic_term;
|
||||
|
||||
return term ? formatAcademicTermLabel(term) : '-';
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
|
||||
@ -25,6 +25,7 @@ import {
|
||||
store,
|
||||
update,
|
||||
} from '@/routes/admin/manage/course-registrations';
|
||||
import { formatAcademicTermLabel } from '@/types/academic-term';
|
||||
import type {
|
||||
CourseRegistration,
|
||||
CourseRegistrationCourseClass,
|
||||
@ -103,7 +104,7 @@ export default function CourseRegistrationIndex({
|
||||
label: 'Periode Akademik',
|
||||
options: academicTerms.map((term) => ({
|
||||
value: String(term.id),
|
||||
label: term.name,
|
||||
label: formatAcademicTermLabel(term),
|
||||
})),
|
||||
},
|
||||
];
|
||||
@ -320,7 +321,7 @@ function RegistrationFields({
|
||||
<SelectContent>
|
||||
{academicTerms.map((term) => (
|
||||
<SelectItem key={term.id} value={String(term.id)}>
|
||||
{term.name}
|
||||
{formatAcademicTermLabel(term)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
|
||||
@ -99,6 +99,7 @@ export default function AdministratorCreate({ roles }: Props) {
|
||||
value={role}
|
||||
/>
|
||||
<Combobox
|
||||
items={roles.map((r) => r.name)}
|
||||
value={role}
|
||||
onValueChange={(v) =>
|
||||
setRole(v ?? '')
|
||||
@ -110,14 +111,14 @@ export default function AdministratorCreate({ roles }: Props) {
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxList>
|
||||
{roles.map((r) => (
|
||||
{(name: string) => (
|
||||
<ComboboxItem
|
||||
key={r.id}
|
||||
value={r.name}
|
||||
key={name}
|
||||
value={name}
|
||||
>
|
||||
{r.name}
|
||||
{name}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
|
||||
@ -119,6 +119,7 @@ export default function AdministratorEdit({ user, roles }: Props) {
|
||||
value={role}
|
||||
/>
|
||||
<Combobox
|
||||
items={roles.map((r) => r.name)}
|
||||
value={role}
|
||||
onValueChange={(v) =>
|
||||
setRole(v ?? '')
|
||||
@ -130,14 +131,14 @@ export default function AdministratorEdit({ user, roles }: Props) {
|
||||
/>
|
||||
<ComboboxContent>
|
||||
<ComboboxList>
|
||||
{roles.map((r) => (
|
||||
{(name: string) => (
|
||||
<ComboboxItem
|
||||
key={r.id}
|
||||
value={r.name}
|
||||
key={name}
|
||||
value={name}
|
||||
>
|
||||
{r.name}
|
||||
{name}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
|
||||
@ -324,7 +324,7 @@ export default function LecturerCreate({ departments }: Props) {
|
||||
Jurusan tidak ditemukan.
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{departments.map((dept) => (
|
||||
{(dept: Department) => (
|
||||
<ComboboxItem
|
||||
key={dept.id}
|
||||
value={dept}
|
||||
@ -333,7 +333,7 @@ export default function LecturerCreate({ departments }: Props) {
|
||||
dept,
|
||||
)}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
|
||||
@ -363,7 +363,7 @@ export default function LecturerEdit({ user, departments }: Props) {
|
||||
Jurusan tidak ditemukan.
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{departments.map((dept) => (
|
||||
{(dept: Department) => (
|
||||
<ComboboxItem
|
||||
key={dept.id}
|
||||
value={dept}
|
||||
@ -372,7 +372,7 @@ export default function LecturerEdit({ user, departments }: Props) {
|
||||
dept,
|
||||
)}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
|
||||
@ -350,7 +350,7 @@ export default function StudentCreate({ departments, lecturers }: Props) {
|
||||
Dosen tidak ditemukan.
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{lecturers.map((lect) => (
|
||||
{(lect: Lecturer) => (
|
||||
<ComboboxItem
|
||||
key={lect.id}
|
||||
value={lect}
|
||||
@ -363,7 +363,7 @@ export default function StudentCreate({ departments, lecturers }: Props) {
|
||||
lect.lecturer_number
|
||||
}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
|
||||
@ -400,7 +400,7 @@ export default function StudentEdit({ user, departments, lecturers }: Props) {
|
||||
Dosen tidak ditemukan.
|
||||
</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{lecturers.map((lect) => (
|
||||
{(lect: Lecturer) => (
|
||||
<ComboboxItem
|
||||
key={lect.id}
|
||||
value={lect}
|
||||
@ -413,7 +413,7 @@ export default function StudentEdit({ user, departments, lecturers }: Props) {
|
||||
lect.lecturer_number
|
||||
}
|
||||
</ComboboxItem>
|
||||
))}
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
|
||||
@ -20,3 +20,12 @@ export type AcademicTerm = {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export function formatAcademicTermLabel(term: {
|
||||
name: string;
|
||||
semester: string;
|
||||
}): string {
|
||||
const label = SemesterLabels[term.semester as Semester];
|
||||
|
||||
return label ? `${term.name} (${label})` : term.name;
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@ export type TuitionInvoiceStudent = {
|
||||
student_number: string;
|
||||
department: { id: number; name: string } | null;
|
||||
user: { profile: { full_name: string } | null } | null;
|
||||
invoiced_term_ids?: number[];
|
||||
};
|
||||
|
||||
export type TuitionInvoice = {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user