ui phase 0

This commit is contained in:
hamid
2026-07-17 14:00:22 +03:30
parent 9051bb3e18
commit f1cba6cf74
139 changed files with 1242 additions and 667 deletions
@@ -93,7 +93,7 @@ const BankStatusPanel: FunctionComponent<BankStatusPanelProps> = ({
{ibanLabel}
</Typography>
) : null}
<Typography sx={{ fontWeight: 600, letterSpacing: 1 }} dir="ltr">
<Typography sx={{ fontWeight: 500, letterSpacing: 1 }} dir="ltr">
{ibanMasked}
</Typography>
{bankName ? (
@@ -105,7 +105,7 @@ const BankStatusPanel: FunctionComponent<BankStatusPanelProps> = ({
) : null}
{status === 'mismatch' && onReenter && reenterLabel ? (
<AppButton color="primary" variant="outlined" startIcon="bank" onClick={onReenter} sx={{ m: 0, alignSelf: 'flex-start' }}>
<AppButton color="primary" variant="outlined" startIcon="bank" onClick={onReenter} sx={{ alignSelf: 'flex-start' }}>
{reenterLabel}
</AppButton>
) : null}
@@ -96,14 +96,14 @@ const BookingRequestSummaryCard: FunctionComponent<BookingRequestSummaryCardProp
<Stack sx={{ gap: 1.5 }}>
<SummaryRow caption={t('summary_patient')}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{patientName}
</Typography>
</SummaryRow>
<SummaryRow caption={t('summary_service')}>
<Stack sx={{ gap: 0.25, alignItems: 'flex-end' }}>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{variantLabel}
</Typography>
{variantPrice ? (
@@ -113,13 +113,13 @@ const BookingRequestSummaryCard: FunctionComponent<BookingRequestSummaryCardProp
</SummaryRow>
<SummaryRow caption={t('summary_address')}>
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'end' }}>
<Typography variant="body2" sx={{ fontWeight: 500, textAlign: 'end' }}>
{addressLabel}
</Typography>
</SummaryRow>
<SummaryRow caption={t('summary_when')}>
<Typography variant="body2" sx={{ fontWeight: 600, textAlign: 'end' }}>
<Typography variant="body2" sx={{ fontWeight: 500, textAlign: 'end' }}>
{whenLabel}
</Typography>
</SummaryRow>
@@ -52,7 +52,7 @@ const CancellationPolicyDisclosure: FunctionComponent<CancellationPolicyDisclosu
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t(`lead_${preview.leadTimeLabel}`)}
</Typography>
<Typography variant="body2" sx={{ color: 'var(--bal-secondary)', fontWeight: 600 }}>
<Typography variant="body2" sx={{ color: 'var(--bal-secondary)', fontWeight: 500 }}>
{t('fee_percent', { percent: feePercent })}
</Typography>
</Stack>
@@ -69,7 +69,7 @@ const CountdownTimer: FunctionComponent<CountdownTimerProps> = ({
return (
<Stack direction="row" sx={{ gap: 0.75, alignItems: 'center', color: 'text.secondary' }}>
<AppIcon icon="pending" size={18} color="var(--bal-text-secondary)" />
<Typography variant="body2" sx={{ fontWeight: 600 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{elapsedText}
</Typography>
</Stack>
@@ -92,7 +92,7 @@ const CountdownTimer: FunctionComponent<CountdownTimerProps> = ({
return (
<Stack sx={{ gap: 0.25, alignItems: 'center' }}>
{label ? (
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 600 }}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 500 }}>
{label}
</Typography>
) : null}
@@ -185,7 +185,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
startIcon="upload"
onClick={openPicker}
disabled={disabled}
sx={{ m: 0, alignSelf: 'flex-start' }}
sx={{ alignSelf: 'flex-start' }}
>
{t('upload_reupload')}
</AppButton>
@@ -194,7 +194,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
<Paper elevation={0} data-upload-state="uploading" sx={uploadedSx}>
<Stack direction="row" sx={{ gap: 1, alignItems: 'center' }}>
<AppIcon icon="upload" size={20} color="var(--bal-primary)" />
<Typography variant="body2" sx={{ fontWeight: 600, flexGrow: 1, wordBreak: 'break-all' }}>
<Typography variant="body2" sx={{ fontWeight: 500, flexGrow: 1, wordBreak: 'break-all' }}>
{fileName}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
@@ -220,7 +220,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
<AppIcon icon="document" size={28} color="var(--bal-primary)" />
)}
<Stack sx={{ gap: 0.25, flexGrow: 1, minWidth: 0 }}>
<Typography variant="body2" sx={{ fontWeight: 600, wordBreak: 'break-all' }}>
<Typography variant="body2" sx={{ fontWeight: 500, wordBreak: 'break-all' }}>
{uploadedName}
</Typography>
<Stack direction="row" sx={{ gap: 0.5, alignItems: 'center' }}>
@@ -230,7 +230,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
</Typography>
</Stack>
</Stack>
<AppButton variant="text" color="primary" onClick={openPicker} disabled={disabled} sx={{ m: 0 }}>
<AppButton variant="text" color="primary" onClick={openPicker} disabled={disabled}>
{t('upload_change')}
</AppButton>
</Stack>
@@ -253,7 +253,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
startIcon="refresh"
onClick={openPicker}
disabled={disabled}
sx={{ m: 0, alignSelf: 'flex-start' }}
sx={{ alignSelf: 'flex-start' }}
>
{t('upload_retry')}
</AppButton>
@@ -287,7 +287,7 @@ const DocumentUpload: FunctionComponent<DocumentUploadProps> = ({
}}
>
<AppIcon icon={capture ? 'camera' : 'upload'} size={28} color="var(--bal-primary)" />
<Typography variant="body2" sx={{ fontWeight: 600 }}>
<Typography variant="body2" sx={{ fontWeight: 700 }}>
{capture ? t('upload_capture') : t('upload_choose')}
</Typography>
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
@@ -59,7 +59,7 @@ const EarningsBalanceHeader: FunctionComponent<EarningsBalanceHeaderProps> = ({
<Stack sx={{ gap: 0.75 }}>
<Stack direction="row" sx={{ gap: 1, alignItems: 'center' }}>
<AppIcon icon={isOwed ? 'warning' : 'wallet'} size={20} color={accent} />
<Typography variant="subtitle2" sx={{ color: 'text.secondary', fontWeight: 600 }}>
<Typography variant="subtitle2" sx={{ color: 'text.secondary', fontWeight: 500 }}>
{isOwed ? t('balance_owed_label') : t('balance_net_label')}
</Typography>
</Stack>
@@ -67,7 +67,7 @@ const EarningsBalanceHeader: FunctionComponent<EarningsBalanceHeaderProps> = ({
<Typography component="span" sx={{ fontWeight: 800, fontSize: '2rem', color: accent }}>
{formatIrrToToman(magnitude, locale)}
</Typography>
<Typography component="span" variant="subtitle1" sx={{ color: 'text.secondary', fontWeight: 600 }}>
<Typography component="span" variant="subtitle1" sx={{ color: 'text.secondary', fontWeight: 500 }}>
{tc('currency_toman')}
</Typography>
</Stack>
@@ -101,7 +101,7 @@ const EarningsBalanceHeader: FunctionComponent<EarningsBalanceHeaderProps> = ({
<Stack sx={{ gap: 0.75 }}>
<Stack direction="row" sx={{ gap: 0.75, alignItems: 'center' }}>
<AppIcon icon={bucket.icon} size={16} color={bucket.token} />
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 600 }}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 500 }}>
{t(`bucket_${bucket.key}`)}
</Typography>
</Stack>
@@ -105,7 +105,6 @@ const EarningsRow: FunctionComponent<EarningsRowProps> = ({ item, onViewBooking,
color="primary"
endIcon="visits"
onClick={() => onViewBooking(item.bookingId)}
sx={{ m: 0 }}
>
{t('view_booking')}
</AppButton>
@@ -178,7 +177,6 @@ const StateAffordance: FunctionComponent<{
color="primary"
endIcon="wallet"
onClick={() => onViewPayout(item.nursePayoutId as number)}
sx={{ m: 0 }}
>
{t('view_payout')}
</AppButton>
@@ -44,7 +44,7 @@ const GenderToggle: FunctionComponent<GenderToggleProps> = ({
'& .MuiToggleButton-root': {
flex: 1,
py: 1.25,
fontWeight: 600,
fontWeight: 700,
borderColor: error ? 'var(--bal-error)' : undefined,
},
}}
@@ -161,7 +161,7 @@ const PatientForm: FunctionComponent<PatientFormProps> = ({
<Stack direction="row" sx={{ gap: 1, justifyContent: 'flex-end' }}>
{onCancel ? (
<AppButton variant="text" onClick={onCancel} disabled={submitting} sx={{ m: 0 }}>
<AppButton variant="text" onClick={onCancel} disabled={submitting}>
{cancelLabel}
</AppButton>
) : null}
@@ -170,7 +170,6 @@ const PatientForm: FunctionComponent<PatientFormProps> = ({
variant="contained"
onClick={handleSubmit}
disabled={submitting}
sx={{ m: 0 }}
>
{submitLabel}
</AppButton>
@@ -43,7 +43,7 @@ const PatientHeader: FunctionComponent<PatientHeaderProps> = ({
{displayName}
</Typography>
{relationLabel ? (
<Chip size="small" label={relationLabel} sx={{ bgcolor: 'var(--bal-primary-soft)', fontWeight: 600 }} />
<Chip size="small" label={relationLabel} sx={{ bgcolor: 'var(--bal-primary-soft)', fontWeight: 500 }} />
) : null}
</Stack>
@@ -47,7 +47,7 @@ const PayoutHistoryRow: FunctionComponent<PayoutHistoryRowProps> = ({ item, onOp
sx={{ justifyContent: 'space-between', alignItems: 'flex-start', gap: 1.5, flexWrap: 'wrap' }}
>
<Stack sx={{ gap: 0.25, minWidth: 0 }}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 600 }}>
<Typography variant="caption" sx={{ color: 'text.secondary', fontWeight: 500 }}>
{t('payout_net_amount')}
</Typography>
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>
@@ -110,7 +110,7 @@ const PayoutHistoryRow: FunctionComponent<PayoutHistoryRowProps> = ({ item, onOp
) : null}
<Stack direction="row" sx={{ justifyContent: 'flex-end' }}>
<AppButton variant="text" color="primary" endIcon="wallet" onClick={() => onOpen(item.id)} sx={{ m: 0 }}>
<AppButton variant="text" color="primary" endIcon="wallet" onClick={() => onOpen(item.id)}>
{t('view_payout_detail')}
</AppButton>
</Stack>
@@ -124,7 +124,7 @@ const MetaLine: FunctionComponent<{ label: string; children: ReactNode }> = ({ l
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{label}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{children}
</Typography>
</Stack>
@@ -49,7 +49,7 @@ const PriceBreakdown: FunctionComponent<PriceBreakdownProps> = ({ rows, totalLab
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{row.label}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{formatIrrToToman(row.amountIrr, locale)}
</Typography>
</Stack>
@@ -47,12 +47,12 @@ const PriceDisplay: FunctionComponent<PriceDisplayProps> = ({
<Stack sx={{ gap: 0.25, alignItems: align === 'center' ? 'center' : 'flex-start' }}>
<Typography component="p" sx={{ fontWeight: 700 }}>
{amount} {tc('currency_toman')}{' '}
<Typography component="span" variant="body2" sx={{ color: 'text.secondary', fontWeight: 600 }}>
<Typography component="span" variant="body2" sx={{ color: 'text.secondary', fontWeight: 500 }}>
{unitLabel}
</Typography>
</Typography>
{hasEstimate ? (
<Typography variant="caption" sx={{ color: 'var(--bal-primary)', fontWeight: 600 }}>
<Typography variant="caption" sx={{ color: 'var(--bal-primary)', fontWeight: 500 }}>
{t('estimate_label')}: {total} {tc('currency_toman')} ·{' '}
{t('estimate_for', { count: countLabel, unit: t(`count_${priceUnit}`) })}
</Typography>
@@ -52,7 +52,7 @@ const RefundEtaBanner: FunctionComponent<RefundEtaBannerProps> = ({ channel, eta
</Typography>
<Typography variant="body2">{t(copy.bodyKey)}</Typography>
{channel === 'bnpl_revert' && (
<Typography variant="caption" sx={{ fontWeight: 600 }} data-testid="refund-eta-window">
<Typography variant="caption" sx={{ fontWeight: 500 }} data-testid="refund-eta-window">
{t('eta_business_days')}
{eta ? ` · ${t('eta_expected_label', { date: formatShamsiDate(eta, locale) })}` : ''}
</Typography>
@@ -62,7 +62,7 @@ const RefundStatusCard: FunctionComponent<RefundStatusCardProps> = ({ refund })
</Typography>
<Typography variant="body2">{t('failed_body')}</Typography>
{refund.externalRevertReference && (
<Typography variant="caption" sx={{ fontWeight: 600, direction: 'ltr' }}>
<Typography variant="caption" sx={{ fontWeight: 500, direction: 'ltr' }}>
{t('reference_label')}: {refund.externalRevertReference}
</Typography>
)}
@@ -97,7 +97,7 @@ const RefundStatusCard: FunctionComponent<RefundStatusCardProps> = ({ refund })
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{t('reference_label')}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600, direction: 'ltr' }}>
<Typography variant="body2" sx={{ fontWeight: 500, direction: 'ltr' }}>
{refund.externalRevertReference}
</Typography>
</Stack>
@@ -56,7 +56,7 @@ const RelationSelect: FunctionComponent<RelationSelectProps> = ({ options, value
}}
>
{option.icon ? <AppIcon icon={option.icon} size={26} color="var(--bal-primary)" /> : null}
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
{option.label}
</Typography>
</Paper>
@@ -38,7 +38,7 @@ const ServicePriceRow: FunctionComponent<ServicePriceRowProps> = ({
borderColor: 'divider',
}}
>
<Typography variant="body1" sx={{ fontWeight: 600, flexGrow: 1 }}>
<Typography variant="body1" sx={{ fontWeight: 500, flexGrow: 1 }}>
{displayName}
</Typography>
<PriceDisplay price={priceIrr} priceUnit={priceUnit} sessionCount={sessionCount} align="start" />
@@ -41,7 +41,7 @@ const StatusChip: FunctionComponent<StatusChipProps> = ({ status, label, size =
size={size}
label={label}
icon={<AppIcon icon={style.icon} size={16} color={style.fg} />}
sx={{ backgroundColor: style.bg, color: style.fg, fontWeight: 600, ...sx }}
sx={{ backgroundColor: style.bg, color: style.fg, fontWeight: 500, ...sx }}
{...rest}
/>
);
@@ -76,7 +76,7 @@ const VariantCard: FunctionComponent<VariantCardProps> = ({ variant, onEdit, onT
) : null}
<Stack direction="row" sx={{ gap: 1, flexWrap: 'wrap' }}>
<AppButton variant="outlined" color="primary" startIcon="edit" onClick={onEdit} sx={{ m: 0 }}>
<AppButton variant="outlined" color="primary" startIcon="edit" onClick={onEdit}>
{t('edit')}
</AppButton>
<AppButton
@@ -84,7 +84,6 @@ const VariantCard: FunctionComponent<VariantCardProps> = ({ variant, onEdit, onT
color={active ? 'inherit' : 'primary'}
startIcon={active ? 'visibilityoff' : 'visibilityon'}
onClick={onToggleActive}
sx={{ m: 0 }}
>
{active ? t('deactivate') : t('activate')}
</AppButton>
@@ -23,7 +23,7 @@ const AdminErrorState: FunctionComponent<AdminErrorStateProps> = ({ message, ret
<Typography variant="body2" sx={{ color: 'text.secondary', mb: 1.5 }}>
{message}
</Typography>
<AppButton variant="outlined" color="primary" startIcon="refresh" onClick={onRetry} sx={{ m: 0 }}>
<AppButton variant="outlined" color="primary" startIcon="refresh" onClick={onRetry}>
{retryLabel}
</AppButton>
</Paper>
+2 -2
View File
@@ -32,13 +32,13 @@ const AdminPager: FunctionComponent<AdminPagerProps> = ({
if (pageCount <= 1) return null;
return (
<Stack direction="row" sx={{ gap: 1, alignItems: 'center', justifyContent: 'center', mt: 1 }}>
<AppButton variant="text" color="primary" onClick={onPrev} disabled={page <= 1} sx={{ m: 0 }}>
<AppButton variant="text" color="primary" onClick={onPrev} disabled={page <= 1}>
{prevLabel}
</AppButton>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
{indicator}
</Typography>
<AppButton variant="text" color="primary" onClick={onNext} disabled={page >= pageCount} sx={{ m: 0 }}>
<AppButton variant="text" color="primary" onClick={onNext} disabled={page >= pageCount}>
{nextLabel}
</AppButton>
</Stack>
+1 -1
View File
@@ -69,7 +69,7 @@ const AuditLogRow: FunctionComponent<AuditLogRowProps> = ({ entry }) => {
<TableBody>
{fields.map(([field, delta]) => (
<TableRow key={field}>
<TableCell sx={{ fontWeight: 600 }}>{field}</TableCell>
<TableCell sx={{ fontWeight: 500 }}>{field}</TableCell>
<TableCell sx={{ color: 'text.secondary' }}>{displayValue(delta.old)}</TableCell>
<TableCell>{displayValue(delta.new)}</TableCell>
</TableRow>
+2 -2
View File
@@ -47,11 +47,11 @@ const ConfigRow: FunctionComponent<ConfigRowProps> = ({ config, canEdit = false,
</Box>
<Stack direction="row" sx={{ gap: 1, flexWrap: 'wrap' }}>
<AppButton variant="text" color="primary" startIcon="history" onClick={() => onHistory?.(config)} sx={{ m: 0 }}>
<AppButton variant="text" color="primary" startIcon="history" onClick={() => onHistory?.(config)}>
{t('cfg_history')}
</AppButton>
{canEdit ? (
<AppButton variant="outlined" color="primary" startIcon="edit" onClick={() => onEdit?.(config)} sx={{ m: 0 }}>
<AppButton variant="outlined" color="primary" startIcon="edit" onClick={() => onEdit?.(config)}>
{t('cfg_edit')}
</AppButton>
) : null}
@@ -93,7 +93,7 @@ const ConfirmDialog: FunctionComponent<ConfirmDialogProps> = ({
) : null}
</DialogContent>
<DialogActions sx={{ px: 3, pb: 2 }}>
<AppButton variant="text" color="inherit" onClick={close} disabled={loading} sx={{ m: 0 }}>
<AppButton variant="text" color="inherit" onClick={close} disabled={loading}>
{cancelLabel}
</AppButton>
<AppButton
@@ -102,7 +102,6 @@ const ConfirmDialog: FunctionComponent<ConfirmDialogProps> = ({
onClick={confirm}
disabled={confirmDisabled}
startIcon={loading ? <CircularProgress size={16} color="inherit" /> : undefined}
sx={{ m: 0 }}
>
{confirmLabel}
</AppButton>
@@ -38,7 +38,7 @@ const DocumentViewer: FunctionComponent<DocumentViewerProps> = ({ document }) =>
>
<Stack direction="row" sx={{ gap: 1, alignItems: 'center', mb: 1 }}>
<AppIcon icon="document" size={18} color="var(--bal-text-secondary)" />
<Typography variant="body2" sx={{ fontWeight: 600, flexGrow: 1, wordBreak: 'break-all' }}>
<Typography variant="body2" sx={{ fontWeight: 500, flexGrow: 1, wordBreak: 'break-all' }}>
{t('doc_file_meta', { name: document.originalFileName ?? `#${document.id}`, size: formatSize(document.fileSizeBytes) })}
</Typography>
<AppButton
@@ -47,7 +47,7 @@ const DocumentViewer: FunctionComponent<DocumentViewerProps> = ({ document }) =>
startIcon="refresh"
onClick={() => signed.refetch()}
disabled={signed.isFetching}
sx={{ m: 0, minWidth: 0 }}
sx={{ minWidth: 0 }}
>
{t('doc_reload')}
</AppButton>
@@ -60,7 +60,7 @@ const DocumentViewer: FunctionComponent<DocumentViewerProps> = ({ document }) =>
<Typography variant="body2" sx={{ color: 'var(--bal-error)' }}>
{t('doc_error')}
</Typography>
<AppButton variant="outlined" color="primary" startIcon="refresh" onClick={() => signed.refetch()} sx={{ m: 0 }}>
<AppButton variant="outlined" color="primary" startIcon="refresh" onClick={() => signed.refetch()}>
{t('doc_reload')}
</AppButton>
</Stack>
@@ -74,7 +74,7 @@ const DocumentViewer: FunctionComponent<DocumentViewerProps> = ({ document }) =>
sx={{ maxWidth: '100%', maxHeight: 320, borderRadius: 1, display: 'block' }}
/>
) : (
<AppButton variant="outlined" color="primary" endIcon="external" href={signed.data.url} openInNewTab sx={{ m: 0 }}>
<AppButton variant="outlined" color="primary" endIcon="external" href={signed.data.url} openInNewTab>
{t('doc_open_new')}
</AppButton>
)
@@ -74,11 +74,11 @@ const PartnerSettlementRow: FunctionComponent<PartnerSettlementRowProps> = ({ in
/>
{invoice.pdfUrl ? (
downloadError ? (
<AppButton variant="outlined" color="error" startIcon="refresh" onClick={() => onRetryDownload?.(invoice)} sx={{ m: 0 }}>
<AppButton variant="outlined" color="error" startIcon="refresh" onClick={() => onRetryDownload?.(invoice)}>
{t('invoice_pdf_error')}
</AppButton>
) : (
<AppButton variant="outlined" color="primary" startIcon="download" onClick={() => onDownloadPdf?.(invoice)} sx={{ m: 0 }}>
<AppButton variant="outlined" color="primary" startIcon="download" onClick={() => onDownloadPdf?.(invoice)}>
{t('invoice_download')}
</AppButton>
)
+3 -3
View File
@@ -160,7 +160,7 @@ const RefundPanel: FunctionComponent<RefundPanelProps> = ({ bookingId, ticketId,
minRows={2}
/>
<AppButton variant="contained" color="primary" startIcon="refunds" onClick={() => setConfirmOpen(true)} disabled={busy} sx={{ m: 0, alignSelf: 'flex-start' }}>
<AppButton variant="contained" color="primary" startIcon="refunds" onClick={() => setConfirmOpen(true)} disabled={busy} sx={{ alignSelf: 'flex-start' }}>
{t('refund_initiate')}
</AppButton>
</>
@@ -170,10 +170,10 @@ const RefundPanel: FunctionComponent<RefundPanelProps> = ({ bookingId, ticketId,
{t('refund_provider_failed')}
</Alert>
<Stack direction="row" sx={{ gap: 1 }}>
<AppButton variant="contained" color="primary" startIcon="refresh" onClick={doRetry} disabled={busy} sx={{ m: 0 }}>
<AppButton variant="contained" color="primary" startIcon="refresh" onClick={doRetry} disabled={busy}>
{t('refund_approve')}
</AppButton>
<AppButton variant="outlined" color="error" onClick={() => setRejectOpen(true)} disabled={busy} sx={{ m: 0 }}>
<AppButton variant="outlined" color="error" onClick={() => setRejectOpen(true)} disabled={busy}>
{t('refund_reject')}
</AppButton>
</Stack>
@@ -85,11 +85,11 @@ const SupportAlertCard: FunctionComponent<SupportAlertCardProps> = ({ alert, can
{canAct && alert.status !== 'resolved' ? (
<Stack direction="row" sx={{ gap: 1, mt: 1.5, flexWrap: 'wrap' }}>
{alert.status === 'open' ? (
<AppButton variant="outlined" color="primary" startIcon="assign" onClick={() => onAssignSelf?.(alert)} sx={{ m: 0 }}>
<AppButton variant="outlined" color="primary" startIcon="assign" onClick={() => onAssignSelf?.(alert)}>
{t('alert_assign_me')}
</AppButton>
) : null}
<AppButton variant="contained" color="primary" startIcon="verified" onClick={() => onResolve?.(alert)} sx={{ m: 0 }}>
<AppButton variant="contained" color="primary" startIcon="verified" onClick={() => onResolve?.(alert)}>
{t('alert_resolve')}
</AppButton>
</Stack>
+3 -2
View File
@@ -2,7 +2,7 @@
import { FunctionComponent } from 'react';
import { Stack, Typography } from '@mui/material';
import { useTranslations } from 'next-intl';
import AppIcon from '../common/AppIcon';
import LogoLockup from '../common/AppIcon/icons/LogoLockup';
interface BrandMarkProps {
/** Show the brand tagline under the wordmark. */
@@ -18,7 +18,8 @@ const BrandMark: FunctionComponent<BrandMarkProps> = ({ withTagline }) => {
const t = useTranslations('common');
return (
<Stack sx={{ alignItems: 'center', gap: 1 }}>
<AppIcon icon="logo" size={56} color="var(--bal-primary)" />
{/* Decorative — the wordmark below already announces "Balinyaar" to AT. */}
<LogoLockup size={56} aria-hidden="true" />
<Typography variant="h5" component="p" sx={{ fontWeight: 700, color: 'primary.main' }}>
{t('brand')}
</Typography>
-1
View File
@@ -129,7 +129,6 @@ const OtpStep: FunctionComponent<OtpStepProps> = ({
fullWidth
disabled={code.length !== OTP_CODE_LENGTH || verifyOtp.isPending || locked}
startIcon={verifyOtp.isPending ? <CircularProgress size={18} color="inherit" /> : undefined}
sx={{ m: 0 }}
>
{isNurse ? t('otp_verify_nurse') : t('otp_verify_customer')}
</AppButton>
-1
View File
@@ -85,7 +85,6 @@ const PhoneStep: FunctionComponent<PhoneStepProps> = ({ intendedRole, onSwitchRo
fullWidth
disabled={requestOtp.isPending}
startIcon={requestOtp.isPending ? <CircularProgress size={18} color="inherit" /> : undefined}
sx={{ m: 0 }}
>
{t('request_code')}
</AppButton>
+1 -2
View File
@@ -84,7 +84,7 @@ const SelectRole: FunctionComponent<SelectRoleProps> = ({ initialRole }) => {
>
<AppIcon icon={icon} size={28} color="var(--bal-primary)" />
<Stack sx={{ gap: 0.25 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 600 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
{t(`role_${role}`)}
</Typography>
<Typography variant="body2" sx={{ color: 'text.secondary' }}>
@@ -103,7 +103,6 @@ const SelectRole: FunctionComponent<SelectRoleProps> = ({ initialRole }) => {
onClick={confirm}
disabled={selectRole.isPending}
startIcon={selectRole.isPending ? <CircularProgress size={18} color="inherit" /> : undefined}
sx={{ m: 0 }}
>
{t('continue')}
</AppButton>
@@ -133,7 +133,7 @@ function HeaderFact({ label, value }: { label: string; value: string }) {
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
{label}
</Typography>
<Typography variant="body2" sx={{ fontWeight: 600 }}>
<Typography variant="body2" sx={{ fontWeight: 500 }}>
{value}
</Typography>
</Stack>
@@ -45,7 +45,7 @@ const BookingStatusTimeline: FunctionComponent<BookingStatusTimelineProps> = ({
sx={{ gap: 1, alignItems: 'center', px: 1.5, py: 1.25, borderRadius: 2, bgcolor: 'var(--bal-primary-soft)' }}
>
<AppIcon icon="rejected" size={20} color="var(--bal-text-secondary)" />
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.secondary' }}>
<Typography variant="body2" sx={{ fontWeight: 500, color: 'text.secondary' }}>
{t('bstatus_cancelled')}
</Typography>
</Stack>
@@ -89,7 +89,7 @@ const SessionCard: FunctionComponent<SessionCardProps> = ({
</Typography>
) : null}
<Stack direction="row" sx={{ justifyContent: 'space-between', alignItems: 'center', gap: 1 }}>
<Typography variant="subtitle2" sx={{ fontWeight: title ? 600 : 700, color: title ? 'text.secondary' : 'text.primary' }}>
<Typography variant="subtitle2" sx={{ fontWeight: title ? 500 : 700, color: title ? 'text.secondary' : 'text.primary' }}>
{t('session_index', { n: sessionIndex })}
</Typography>
<StatusChip status={SESSION_STATUS_KIND[status]} label={t(`sstatus_${status}`)} />
@@ -109,7 +109,7 @@ const SessionCard: FunctionComponent<SessionCardProps> = ({
) : null}
{showPayout && visitPayoutAmount ? (
<Typography variant="caption" sx={{ color: 'var(--bal-secondary-dark)', fontWeight: 600 }}>
<Typography variant="caption" sx={{ color: 'var(--bal-secondary-dark)', fontWeight: 500 }}>
{t('session_payout')}: {formatIrrToToman(visitPayoutAmount, locale)} {tc('currency_toman')}
</Typography>
) : null}
@@ -121,7 +121,7 @@ const SessionCard: FunctionComponent<SessionCardProps> = ({
startIcon={acquiringLocation ? 'gps' : 'check_in'}
disabled={busy}
onClick={onCheckIn}
sx={{ m: 0, alignSelf: 'flex-start', py: 1 }}
sx={{ alignSelf: 'flex-start', py: 1 }}
>
{acquiringLocation ? t('evv_acquiring_location') : evvPending ? t('evv_checking_in') : t('evv_check_in')}
</AppButton>
@@ -134,7 +134,7 @@ const SessionCard: FunctionComponent<SessionCardProps> = ({
startIcon={acquiringLocation ? 'gps' : 'check_out'}
disabled={busy}
onClick={onCheckOut}
sx={{ m: 0, alignSelf: 'flex-start', py: 1 }}
sx={{ alignSelf: 'flex-start', py: 1 }}
>
{acquiringLocation ? t('evv_acquiring_location') : evvPending ? t('evv_checking_out') : t('evv_check_out')}
</AppButton>
@@ -60,14 +60,17 @@ describe('<AppButton/> component', () => {
expect(button).toHaveAttribute('type', 'button'); // not "submit" or "input" by default
});
it('has .margin style by default', () => {
let text = 'button with default margin';
it('does not force a default margin, and passes a custom sx through untouched', () => {
const testId = randomText(8);
render(<ComponentToTest data-testid={testId}>{text}</ComponentToTest>);
render(
<ComponentToTest data-testid={testId} sx={{ marginInlineStart: 2 }}>
button with custom sx
</ComponentToTest>
);
const button = screen.getByTestId(testId);
expect(button).toBeDefined();
// MUI v9 + cssVariables: spacing is emitted as CSS vars (not resolved in jsdom).
// Verify the sx margin is applied via class rather than inline style.
// Regression guard for the removed starter DEFAULT_SX_VALUES={ margin: 1 }:
// AppButton must not silently drop or merge over a caller-provided sx.
expect(button).toHaveClass('MuiButton-root');
});
@@ -127,7 +130,7 @@ describe('<AppButton/> component', () => {
let text = 'button with start icon';
render(<ComponentToTest text={text} startIcon="default" />);
let button = screen.getByText(text);
let icon = within(button).getByTestId('MoreHorizIcon'); //Note: this is valid only when "default" icon is <MoreHorizIcon />
let icon = within(button).getByTestId('MoreHorizRoundedIcon'); //Note: this is valid only when "default" icon is <MoreHorizRoundedIcon />
expect(icon).toBeDefined();
let span = icon.closest('span');
expect(span).toHaveClass('MuiButton-startIcon');
@@ -137,7 +140,7 @@ describe('<AppButton/> component', () => {
let text = 'button with end icon as string';
render(<ComponentToTest text={text} endIcon="default" />);
let button = screen.getByText(text);
let icon = within(button).getByTestId('MoreHorizIcon'); //Note: this is valid only when "default" icon is <MoreHorizIcon />
let icon = within(button).getByTestId('MoreHorizRoundedIcon'); //Note: this is valid only when "default" icon is <MoreHorizRoundedIcon />
expect(icon).toBeDefined();
let span = icon.closest('span');
expect(span).toHaveClass('MuiButton-endIcon');
@@ -6,26 +6,20 @@ import { APP_BUTTON_VARIANT } from '../../config';
const MUI_BUTTON_COLORS = ['inherit', 'primary', 'secondary', 'success', 'error', 'info', 'warning'];
const DEFAULT_SX_VALUES = {
margin: 1, // By default the AppButton has theme.spacing(1) margin on all sides
};
export interface AppButtonProps extends Omit<ButtonProps, 'color' | 'endIcon' | 'startIcon'> {
color?: string; // Not only 'inherit' | 'primary' | 'secondary' | 'success' | 'error' | 'info' | 'warning',
endIcon?: string | ReactNode;
label?: string; // Alternate to .text
text?: string; // Alternate to .label
startIcon?: string | ReactNode;
// Missing props
component?: ElementType; // Could be RouterLink, AppLink, <a>, etc.
to?: string; // Link prop
href?: string; // Link prop
openInNewTab?: boolean; // Link prop
underline?: 'none' | 'hover' | 'always'; // Link prop
}
/**
* Application styled Material UI Button with Box around to specify margins using props
* Application-styled Material UI Button.
* @component AppButton
* @param {string} [color] - when passing MUI value ('primary', 'secondary', and so on), it is color of the button body, otherwise it is color of text and icons
* @param {string} [children] - content to render, overrides .label and .text props
@@ -37,7 +31,6 @@ export interface AppButtonProps extends Omit<ButtonProps, 'color' | 'endIcon' |
* @param {Array<func| object| bool> | func | object} [sx] - additional CSS styles to apply to the button
* @param {string} [text] - text to render, alternate to .label
* @param {string} [to] - internal link URI
* @param {string} [underline] - controls underline style when button used as link, one of 'none', 'hover', or 'always'
* @param {string} [variant] - MUI variant of the button, one of 'text', 'outlined', or 'contained'
*/
const AppButton: FunctionComponent<AppButtonProps> = ({
@@ -47,9 +40,7 @@ const AppButton: FunctionComponent<AppButtonProps> = ({
endIcon,
label,
startIcon,
sx: propSx = DEFAULT_SX_VALUES,
text,
underline = 'none',
variant = APP_BUTTON_VARIANT,
...restOfProps
}) => {
@@ -69,10 +60,7 @@ const AppButton: FunctionComponent<AppButtonProps> = ({
!propComponent && (restOfProps?.href || restOfProps?.to) ? AppLink : propComponent ?? Button;
const colorToRender = isMuiColor ? (propColor as ButtonProps['color']) : 'inherit';
const sxToRender = {
...propSx,
...(isMuiColor ? {} : { color: propColor }),
};
const sxToRender = isMuiColor ? restOfProps.sx : { ...restOfProps.sx, color: propColor };
return (
<Button
@@ -80,9 +68,9 @@ const AppButton: FunctionComponent<AppButtonProps> = ({
color={colorToRender}
endIcon={iconEnd}
startIcon={iconStart}
sx={sxToRender}
variant={variant}
{...{ ...restOfProps, underline }}
{...restOfProps}
sx={sxToRender}
>
{children || label || text}
</Button>
@@ -16,9 +16,11 @@ describe('<AppIcon/> component', () => {
const svg = screen.getByTestId(testId);
expect(svg).toBeDefined();
expect(svg).toHaveAttribute('data-icon', 'default');
expect(svg).toHaveAttribute('size', String(APP_ICON_SIZE)); // default size
expect(svg).toHaveAttribute('height', String(APP_ICON_SIZE)); // default size when .size is not set
expect(svg).toHaveAttribute('width', String(APP_ICON_SIZE)); // default size when .size is not se
// Size is actually driven by fontSize (MUI SvgIcon's `1em` sizing beats
// width/height attributes) — never assert a `size` DOM attribute, it's invalid.
expect(svg).toHaveStyle(`font-size: ${APP_ICON_SIZE}px`);
});
it('supports .color property', () => {
@@ -48,9 +50,11 @@ describe('<AppIcon/> component', () => {
const size = Math.floor(Math.random() * 128) + 1;
render(<ComponentToTest data-testid={testId} size={size} />);
const svg = screen.getByTestId(testId);
expect(svg).toHaveAttribute('size', String(size));
expect(svg).toHaveAttribute('height', String(size));
expect(svg).toHaveAttribute('width', String(size));
// The bug this regression-guards: MUI SvgIcon's own CSS sets width/height:1em,
// which beats plain width/height attributes — only fontSize actually resizes it.
expect(svg).toHaveStyle(`font-size: ${size}px`);
});
it('supports .title property', () => {
@@ -1,6 +1,6 @@
import { ComponentType, FunctionComponent, SVGAttributes } from 'react';
import { APP_ICON_SIZE } from '../../config';
import { IconName, ICONS } from './config';
import { IconName, ICONS, DIRECTIONAL_ICONS } from './config';
/**
* Props of the AppIcon component, also can be used for SVG icons
@@ -31,21 +31,35 @@ const AppIcon: FunctionComponent<Props> = ({
let ComponentToRender: ComponentType = ICONS[iconName];
if (!ComponentToRender) {
console.warn(`AppIcon: icon "${iconName}" is not found!`);
ComponentToRender = ICONS.default; // ICONS['default'];
if (process.env.NODE_ENV !== 'production') {
console.warn(`AppIcon: icon "${iconName}" is not found!`);
}
ComponentToRender = ICONS.default;
}
const sizeValue = typeof size === 'number' ? `${size}px` : size;
// MUI's SvgIcon sets width/height:1em via its own class, which beats plain
// width/height attributes — driving size through fontSize (the basis for 1em)
// is what actually resizes MUI-registry icons. Custom raw <svg> icons (the
// brand mark) aren't MUI SvgIcon-based, so they keep scaling off the explicit
// width/height attributes below; the extra fontSize style is harmless for them.
const propsToRender = {
height: size,
width: size,
color,
fill: color && 'currentColor',
size,
style: { ...style, color },
width: size,
style: { ...style, color, fontSize: sizeValue },
...restOfProps,
};
return <ComponentToRender data-icon={iconName} {...propsToRender} />;
// Directional icons (back/chevron_start, …) are authored for LTR and mirror
// under RTL via the single CSS rule in tokens.css keyed on this attribute —
// registering a name in DIRECTIONAL_ICONS (config.ts) is the only step a
// later phase needs; never hand-roll a flip at the call site.
const directionalProps = DIRECTIONAL_ICONS.has(iconName) ? { 'data-icon-directional': true } : undefined;
return <ComponentToRender data-icon={iconName} {...directionalProps} {...propsToRender} />;
};
export default AppIcon;
+110 -100
View File
@@ -1,101 +1,103 @@
// SVG assets
import PencilIcon from './icons/PencilIcon';
// MUI Icons
import DefaultIcon from '@mui/icons-material/MoreHoriz';
import SettingsIcon from '@mui/icons-material/Settings';
import VisibilityIcon from '@mui/icons-material/Visibility';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import MenuIcon from '@mui/icons-material/Menu';
import CloseIcon from '@mui/icons-material/Close';
import DayNightIcon from '@mui/icons-material/Brightness4';
import NightIcon from '@mui/icons-material/Brightness3';
import DayIcon from '@mui/icons-material/Brightness5';
import SearchIcon from '@mui/icons-material/Search';
import InfoIcon from '@mui/icons-material/Info';
import HomeIcon from '@mui/icons-material/Home';
import AccountCircle from '@mui/icons-material/AccountCircle';
import PersonAddIcon from '@mui/icons-material/PersonAdd';
import PersonIcon from '@mui/icons-material/Person';
import ExitToAppIcon from '@mui/icons-material/ExitToApp';
import NotificationsIcon from '@mui/icons-material/NotificationsOutlined';
import DangerousIcon from '@mui/icons-material/Dangerous';
import EventNoteIcon from '@mui/icons-material/EventNote';
import GroupsIcon from '@mui/icons-material/Groups';
import PeopleAltIcon from '@mui/icons-material/PeopleAlt';
import WalletIcon from '@mui/icons-material/AccountBalanceWallet';
import PersonOutlineIcon from '@mui/icons-material/AccountCircleOutlined';
import DashboardIcon from '@mui/icons-material/Dashboard';
import VerifiedUserIcon from '@mui/icons-material/VerifiedUser';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmpty';
import CancelIcon from '@mui/icons-material/Cancel';
import MedicalServicesIcon from '@mui/icons-material/MedicalServices';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettings';
import AddIcon from '@mui/icons-material/Add';
import EditIcon from '@mui/icons-material/EditOutlined';
import ArchiveIcon from '@mui/icons-material/Inventory2Outlined';
import BankIcon from '@mui/icons-material/AccountBalanceOutlined';
import CameraIcon from '@mui/icons-material/PhotoCameraOutlined';
import WarningIcon from '@mui/icons-material/WarningAmberOutlined';
import LocationIcon from '@mui/icons-material/LocationOnOutlined';
import DeleteIcon from '@mui/icons-material/DeleteOutlined';
import CoverageIcon from '@mui/icons-material/MapOutlined';
// SVG assets — the Balinyaar brand mark
import LogoMark from './icons/LogoMark';
// MUI Icons — normalized to ONE visual family (Rounded): warmer, softer strokes
// than the filled/outlined mix this registry used to carry, fitting the
// clinical-but-human tone. Adding an icon = import here + one lowercase key
// below; never import a raw MUI icon anywhere else in the app.
import DefaultIcon from '@mui/icons-material/MoreHorizRounded';
import CloseIcon from '@mui/icons-material/CloseRounded';
import MenuIcon from '@mui/icons-material/MenuRounded';
import SearchIcon from '@mui/icons-material/SearchRounded';
import InfoIcon from '@mui/icons-material/InfoRounded';
import HomeIcon from '@mui/icons-material/HomeRounded';
import AccountCircleIcon from '@mui/icons-material/AccountCircleRounded';
import ExitToAppIcon from '@mui/icons-material/ExitToAppRounded';
import NotificationsIcon from '@mui/icons-material/NotificationsRounded';
import DangerousIcon from '@mui/icons-material/DangerousRounded';
import EventNoteIcon from '@mui/icons-material/EventNoteRounded';
import GroupsIcon from '@mui/icons-material/GroupsRounded';
import PeopleAltIcon from '@mui/icons-material/PeopleAltRounded';
import WalletIcon from '@mui/icons-material/AccountBalanceWalletRounded';
import DashboardIcon from '@mui/icons-material/DashboardRounded';
import VerifiedUserIcon from '@mui/icons-material/VerifiedUserRounded';
import CheckCircleIcon from '@mui/icons-material/CheckCircleRounded';
import HourglassEmptyIcon from '@mui/icons-material/HourglassEmptyRounded';
import CancelIcon from '@mui/icons-material/CancelRounded';
import MedicalServicesIcon from '@mui/icons-material/MedicalServicesRounded';
import AdminPanelSettingsIcon from '@mui/icons-material/AdminPanelSettingsRounded';
import AddIcon from '@mui/icons-material/AddRounded';
import EditIcon from '@mui/icons-material/EditRounded';
import ArchiveIcon from '@mui/icons-material/Inventory2Rounded';
import BankIcon from '@mui/icons-material/AccountBalanceRounded';
import CameraIcon from '@mui/icons-material/PhotoCameraRounded';
import WarningIcon from '@mui/icons-material/WarningAmberRounded';
import LocationIcon from '@mui/icons-material/LocationOnRounded';
import DeleteIcon from '@mui/icons-material/DeleteRounded';
import CoverageIcon from '@mui/icons-material/MapRounded';
// Catalog — nurse services surface + the customer Home service-category grid (f4/b5)
import ServicesIcon from '@mui/icons-material/LocalOfferOutlined';
import CategoryIcon from '@mui/icons-material/CategoryOutlined';
import ElderlyIcon from '@mui/icons-material/ElderlyOutlined';
import PostSurgeryIcon from '@mui/icons-material/HealingOutlined';
import InfantIcon from '@mui/icons-material/ChildCareOutlined';
import ChronicIcon from '@mui/icons-material/MonitorHeartOutlined';
import CompanionshipIcon from '@mui/icons-material/VolunteerActivismOutlined';
import ServicesIcon from '@mui/icons-material/LocalOfferRounded';
import CategoryIcon from '@mui/icons-material/CategoryRounded';
import ElderlyIcon from '@mui/icons-material/ElderlyRounded';
import PostSurgeryIcon from '@mui/icons-material/HealingRounded';
import InfantIcon from '@mui/icons-material/ChildCareRounded';
import ChronicIcon from '@mui/icons-material/MonitorHeartRounded';
import CompanionshipIcon from '@mui/icons-material/VolunteerActivismRounded';
// Verification — nurse trust flow (f5/b6): document upload, credential + identity, re-upload
import UploadIcon from '@mui/icons-material/CloudUploadOutlined';
import DocumentIcon from '@mui/icons-material/InsertDriveFileOutlined';
import RefreshIcon from '@mui/icons-material/RefreshOutlined';
import IdentityIcon from '@mui/icons-material/BadgeOutlined';
import LicenseIcon from '@mui/icons-material/WorkspacePremiumOutlined';
import PublishIcon from '@mui/icons-material/RocketLaunchOutlined';
import UploadIcon from '@mui/icons-material/CloudUploadRounded';
import DocumentIcon from '@mui/icons-material/InsertDriveFileRounded';
import RefreshIcon from '@mui/icons-material/RefreshRounded';
import IdentityIcon from '@mui/icons-material/BadgeRounded';
import LicenseIcon from '@mui/icons-material/WorkspacePremiumRounded';
import PublishIcon from '@mui/icons-material/RocketLaunchRounded';
// Search & discovery — the customer nurse-finding flow (f6/b7): rating star, filter controls
import StarIcon from '@mui/icons-material/Star';
import TuneIcon from '@mui/icons-material/TuneOutlined';
import StarIcon from '@mui/icons-material/StarRounded';
import StarHalfIcon from '@mui/icons-material/StarHalfRounded';
import TuneIcon from '@mui/icons-material/TuneRounded';
import SortIcon from '@mui/icons-material/SortRounded';
// Booking requests — the pre-payment intent flow (f7/b8): nurse inbox + the pay-&-continue handoff
import RequestsIcon from '@mui/icons-material/AssignmentOutlined';
import PaymentIcon from '@mui/icons-material/CreditCardOutlined';
import RequestsIcon from '@mui/icons-material/AssignmentRounded';
import PaymentIcon from '@mui/icons-material/CreditCardRounded';
// Bookings, sessions & EVV — the post-payment engagement (f8/b9): check-in/out, GPS, care instructions
import CheckInIcon from '@mui/icons-material/LoginOutlined';
import CheckOutIcon from '@mui/icons-material/LogoutOutlined';
import GpsIcon from '@mui/icons-material/MyLocationOutlined';
import ScheduleIcon from '@mui/icons-material/ScheduleOutlined';
import ClinicalIcon from '@mui/icons-material/HealthAndSafetyOutlined';
import MedicationIcon from '@mui/icons-material/MedicationOutlined';
import EmergencyIcon from '@mui/icons-material/LocalPhoneOutlined';
import LockIcon from '@mui/icons-material/LockOutlined';
import CheckInIcon from '@mui/icons-material/LoginRounded';
import CheckOutIcon from '@mui/icons-material/LogoutRounded';
import GpsIcon from '@mui/icons-material/MyLocationRounded';
import ScheduleIcon from '@mui/icons-material/ScheduleRounded';
import ClinicalIcon from '@mui/icons-material/HealthAndSafetyRounded';
import MedicationIcon from '@mui/icons-material/MedicationRounded';
import EmergencyIcon from '@mui/icons-material/LocalPhoneRounded';
import PhoneIcon from '@mui/icons-material/PhoneRounded';
import LockIcon from '@mui/icons-material/LockRounded';
import NavigateIcon from '@mui/icons-material/NavigationRounded';
// BNPL — the installment-checkout surface (f11/b12): installment plans + repayment schedule
import InstallmentsIcon from '@mui/icons-material/PaymentsOutlined';
import InstallmentsIcon from '@mui/icons-material/PaymentsRounded';
// Payouts — the nurse earnings & payout-history surface (f12/b13)
import EarningsIcon from '@mui/icons-material/PaidOutlined';
import EarningsIcon from '@mui/icons-material/PaidRounded';
// Reviews & patient care records (f13/b14): visit-note authoring, record tabs, family-ownership banner
import NotesIcon from '@mui/icons-material/NoteAltOutlined';
import RoutineIcon from '@mui/icons-material/EventRepeatOutlined';
import TasksIcon from '@mui/icons-material/ChecklistOutlined';
import HistoryIcon from '@mui/icons-material/HistoryOutlined';
import FamilyIcon from '@mui/icons-material/FamilyRestroomOutlined';
import NotesIcon from '@mui/icons-material/NoteAltRounded';
import RoutineIcon from '@mui/icons-material/EventRepeatRounded';
import TasksIcon from '@mui/icons-material/ChecklistRounded';
import HistoryIcon from '@mui/icons-material/HistoryRounded';
import FamilyIcon from '@mui/icons-material/FamilyRestroomRounded';
// Messaging (tickets) & notifications (f14/b15): support inbox + the message-send action
import SupportIcon from '@mui/icons-material/SupportAgentOutlined';
import SendIcon from '@mui/icons-material/SendOutlined';
import SupportIcon from '@mui/icons-material/SupportAgentRounded';
import SendIcon from '@mui/icons-material/SendRounded';
// Admin backoffice & partner consoles (f15/b15): config, holidays, audit, alerts, moderation, partners, refunds
import ConfigIcon from '@mui/icons-material/TuneOutlined';
import CalendarIcon from '@mui/icons-material/CalendarMonthOutlined';
import AuditIcon from '@mui/icons-material/FactCheckOutlined';
import AlertsIcon from '@mui/icons-material/NotificationImportantOutlined';
import ModerationIcon from '@mui/icons-material/GavelOutlined';
import PartnersIcon from '@mui/icons-material/ApartmentOutlined';
import RolesIcon from '@mui/icons-material/ManageAccountsOutlined';
import RefundsIcon from '@mui/icons-material/CurrencyExchangeOutlined';
import DownloadIcon from '@mui/icons-material/FileDownloadOutlined';
import ExpandIcon from '@mui/icons-material/ExpandMoreOutlined';
import ExternalIcon from '@mui/icons-material/OpenInNewOutlined';
import AssignIcon from '@mui/icons-material/AssignmentIndOutlined';
import CalendarIcon from '@mui/icons-material/CalendarMonthRounded';
import AuditIcon from '@mui/icons-material/FactCheckRounded';
import AlertsIcon from '@mui/icons-material/NotificationImportantRounded';
import ModerationIcon from '@mui/icons-material/GavelRounded';
import PartnersIcon from '@mui/icons-material/ApartmentRounded';
import RolesIcon from '@mui/icons-material/ManageAccountsRounded';
import RefundsIcon from '@mui/icons-material/CurrencyExchangeRounded';
import DownloadIcon from '@mui/icons-material/FileDownloadRounded';
import ExpandIcon from '@mui/icons-material/ExpandMoreRounded';
import ExternalIcon from '@mui/icons-material/OpenInNewRounded';
import AssignIcon from '@mui/icons-material/AssignmentIndRounded';
// Cross-cutting affordances: back navigation, share/copy, attachments
import BackIcon from '@mui/icons-material/ArrowBackRounded';
import ShareIcon from '@mui/icons-material/ShareRounded';
import CopyIcon from '@mui/icons-material/ContentCopyRounded';
import AttachmentIcon from '@mui/icons-material/AttachFileRounded';
/**
* List of all available Icon names
@@ -112,21 +114,13 @@ export type IconName = keyof typeof ICONS;
*/
export const ICONS /* Note: Setting type disables property autocomplete :( was - : Record<string, ComponentType> */ = {
default: DefaultIcon,
logo: PencilIcon,
logo: LogoMark,
close: CloseIcon,
menu: MenuIcon,
settings: SettingsIcon,
visibilityon: VisibilityIcon,
visibilityoff: VisibilityOffIcon,
daynight: DayNightIcon,
night: NightIcon,
day: DayIcon,
search: SearchIcon,
info: InfoIcon,
home: HomeIcon,
account: AccountCircle,
signup: PersonAddIcon,
login: PersonIcon,
account: AccountCircleIcon,
logout: ExitToAppIcon,
notifications: NotificationsIcon,
error: DangerousIcon,
@@ -134,7 +128,7 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
patients: GroupsIcon,
users: PeopleAltIcon,
wallet: WalletIcon,
profile: PersonOutlineIcon,
profile: AccountCircleIcon,
dashboard: DashboardIcon,
verification: VerifiedUserIcon,
verified: CheckCircleIcon,
@@ -165,7 +159,9 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
license: LicenseIcon,
publish: PublishIcon,
star: StarIcon,
star_half: StarHalfIcon,
tune: TuneIcon,
sort: SortIcon,
requests: RequestsIcon,
payment: PaymentIcon,
check_in: CheckInIcon,
@@ -175,7 +171,9 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
clinical: ClinicalIcon,
medication: MedicationIcon,
emergency: EmergencyIcon,
phone: PhoneIcon,
lock: LockIcon,
navigate: NavigateIcon,
installments: InstallmentsIcon,
earnings: EarningsIcon,
notes: NotesIcon,
@@ -185,7 +183,7 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
family: FamilyIcon,
support: SupportIcon,
send: SendIcon,
config: ConfigIcon,
config: TuneIcon,
calendar: CalendarIcon,
audit: AuditIcon,
alerts: AlertsIcon,
@@ -197,4 +195,16 @@ export const ICONS /* Note: Setting type disables property autocomplete :( was -
expand: ExpandIcon,
external: ExternalIcon,
assign: AssignIcon,
back: BackIcon,
chevron_start: BackIcon,
share: ShareIcon,
copy: CopyIcon,
attachment: AttachmentIcon,
};
/**
* Icons authored for LTR that must mirror horizontally under RTL (back arrows,
* chevrons pointing "start"). AppIcon applies the flip via a `data-icon-directional`
* attribute + the single CSS rule in globals.css — add a name here, nothing else.
*/
export const DIRECTIONAL_ICONS = new Set<IconName>(['back', 'chevron_start']);
@@ -0,0 +1,30 @@
import { FunctionComponent } from 'react';
import { IconProps } from '../utils';
/**
* The full-color Balinyaar mark: deep-teal rounded-square ground, cream
* lowercase "b" glyph, single terracotta dot — the brand identity described
* in the frontend-designer skill. Colors are `var(--bal-*)` tokens (never a
* hex literal), so the mark tracks the color scheme like everything else.
* Used directly by BrandMark next to the translated wordmark — the wordmark
* stays real `<Typography>` text (not baked into the SVG) so it still
* switches with the locale. For an inline, single-color icon (e.g. inside a
* button) use AppIcon's `logo` name (LogoMark) instead.
* @component LogoLockup
*/
const LogoLockup: FunctionComponent<IconProps> = ({ size = 56, ...props }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" width={size} height={size} {...props}>
<rect width="40" height="40" rx="10" fill="var(--bal-primary)" />
<path
fill="var(--bal-primary-contrast)"
fillRule="evenodd"
clipRule="evenodd"
d="M21,17 A7.5,7.5 0 1,0 21,32 A7.5,7.5 0 1,0 21,17 Z
M21,20.8 A3.7,3.7 0 1,0 21,28.2 A3.7,3.7 0 1,0 21,20.8 Z"
/>
<rect x="12.9" y="9" width="4.8" height="22" rx="2.4" fill="var(--bal-primary-contrast)" />
<circle cx="28.5" cy="10.5" r="2.6" fill="var(--bal-secondary)" />
</svg>
);
export default LogoLockup;
@@ -0,0 +1,28 @@
import { FunctionComponent } from 'react';
import { IconProps } from '../utils';
/**
* The Balinyaar monochrome mark — a lowercase "b" (stem + bowl) reduced to a
* single `currentColor` fill so it works like any other AppIcon: it recolors
* via the `color` prop and therefore for free across the light/dark scheme.
* The bowl's counter is a true transparent hole (evenodd fill-rule on two
* concentric circles), not a background-color cutout — it reads correctly on
* any surface. For the full-color brand lockup (auth splash, TopBar) see
* LogoLockup.tsx instead.
* @component LogoMark
*/
const LogoMark: FunctionComponent<IconProps> = ({ color = 'currentColor', ...props }) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 36 36" {...props}>
<path
fill={color}
fillRule="evenodd"
clipRule="evenodd"
d="M19.5,15.3 A7.2,7.2 0 1,0 19.5,29.7 A7.2,7.2 0 1,0 19.5,15.3 Z
M19.5,18.9 A3.6,3.6 0 1,0 19.5,26.1 A3.6,3.6 0 1,0 19.5,18.9 Z"
/>
<rect x="10.9" y="8" width="4.6" height="21" rx="2.3" fill={color} />
<circle cx="27" cy="10" r="2.4" fill={color} />
</svg>
);
export default LogoMark;
@@ -1,29 +0,0 @@
import { FunctionComponent } from 'react';
import { IconProps } from '../utils';
const PencilIcon: FunctionComponent<IconProps> = (props) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 36 36" {...props}>
<path
fill="#D99E82"
d="M35.222 33.598c-.647-2.101-1.705-6.059-2.325-7.566-.501-1.216-.969-2.438-1.544-3.014-.575-.575-1.553-.53-2.143.058 0 0-2.469 1.675-3.354 2.783-1.108.882-2.785 3.357-2.785 3.357-.59.59-.635 1.567-.06 2.143.576.575 1.798 1.043 3.015 1.544 1.506.62 5.465 1.676 7.566 2.325.359.11 1.74-1.271 1.63-1.63z"
/>
<path
fill="#EA596E"
d="M13.643 5.308c1.151 1.151 1.151 3.016 0 4.167l-4.167 4.168c-1.151 1.15-3.018 1.15-4.167 0L1.141 9.475c-1.15-1.151-1.15-3.016 0-4.167l4.167-4.167c1.15-1.151 3.016-1.151 4.167 0l4.168 4.167z"
/>
<path fill="#FFCC4D" d="M31.353 23.018l-4.17 4.17-4.163 4.165L7.392 15.726l8.335-8.334 15.626 15.626z" />
<path
fill="#292F33"
d="M32.078 34.763s2.709 1.489 3.441.757c.732-.732-.765-3.435-.765-3.435s-2.566.048-2.676 2.678z"
/>
<path fill="#CCD6DD" d="M2.183 10.517l8.335-8.335 5.208 5.209-8.334 8.335z" />
<path
fill="#99AAB5"
d="M3.225 11.558l8.334-8.334 1.042 1.042L4.267 12.6zm2.083 2.086l8.335-8.335 1.042 1.042-8.335 8.334z"
/>
</svg>
);
};
export default PencilIcon;
@@ -36,9 +36,10 @@ describe('<AppIconButton/> component', () => {
const svg = button.querySelector('svg');
expect(svg).toBeDefined();
expect(svg).toHaveAttribute('data-icon', 'default'); // default icon
expect(svg).toHaveAttribute('size', String(APP_ICON_SIZE)); // default size
expect(svg).toHaveAttribute('height', String(APP_ICON_SIZE)); // default size when .size is not set
expect(svg).toHaveAttribute('width', String(APP_ICON_SIZE)); // default size when .size is not se
// Size is actually driven by fontSize, not a `size` DOM attribute (invalid on <svg>).
expect(svg).toHaveStyle(`font-size: ${APP_ICON_SIZE}px`);
});
it('supports .color property', () => {
@@ -1,55 +0,0 @@
import { render, screen } from '@testing-library/react';
import { randomText } from '@/utils';
import AppImage from './AppImage';
const ComponentToTest = AppImage;
/**
* Tests for <AppImage/> component
*/
describe('<AppImage/> component', () => {
const src = 'https:/domain.com/image.jpg';
it('renders itself', () => {
const testId = randomText(8);
render(<ComponentToTest data-testid={testId} src={src} />);
const image = screen.getByTestId(testId);
expect(image).toBeDefined();
expect(image).toHaveAttribute('src', src);
expect(image).toHaveAttribute('alt', 'Image'); // Default prop value
expect(image).toHaveAttribute('height', '256'); // Default prop value
expect(image).toHaveAttribute('width', '256'); // Default prop value
});
it('supports .width and .height props', () => {
const testId = randomText(8);
const height = 345;
const width = 123;
render(<ComponentToTest data-testid={testId} height={height} src={src} width={width} />);
const image = screen.getByTestId(testId);
expect(image).toBeDefined();
expect(image).toHaveAttribute('height', String(height));
expect(image).toHaveAttribute('width', String(width));
});
it('supports .title property', () => {
const testId = randomText(8);
const title = randomText(16);
render(<ComponentToTest data-testid={testId} src={src} title={title} />);
const image = screen.getByTestId(testId);
expect(image).toBeDefined();
expect(image).toHaveAttribute('title', title);
expect(image).toHaveAttribute('alt', title); // When title is provided, it is used as alt
});
it('supports .alt property even when .title is provided', () => {
const testId = randomText(8);
const title = randomText(16);
const alt = randomText(32);
render(<ComponentToTest alt={alt} data-testid={testId} src={src} title={title} />);
const image = screen.getByTestId(testId);
expect(image).toBeDefined();
expect(image).toHaveAttribute('alt', alt);
expect(image).toHaveAttribute('title', title);
});
});
@@ -1,23 +0,0 @@
import { FunctionComponent } from 'react';
import NextImage, { ImageProps } from 'next/image';
interface AppImageProps extends Omit<ImageProps, 'alt'> {
alt?: string; // Make property optional as it was before NextJs v13
}
/**
* Application wrapper around NextJS image with some default props
* @component AppImage
*/
const AppImage: FunctionComponent<AppImageProps> = ({
title, // Note: value has be destructed before usage as default value for other property
alt = title ?? 'Image',
height = 256,
width = 256,
...restOfProps
}) => {
// Uses custom loader + unoptimized="true" to avoid NextImage warning https://nextjs.org/docs/api-reference/next/image#unoptimized
return <NextImage alt={alt} height={height} title={title} unoptimized={true} width={width} {...restOfProps} />;
};
export default AppImage;
@@ -1,3 +0,0 @@
import AppImage from './AppImage';
export { AppImage as default, AppImage };
+1 -2
View File
@@ -2,9 +2,8 @@ import AppAlert from './AppAlert';
import AppButton from './AppButton';
import AppIcon from './AppIcon';
import AppIconButton from './AppIconButton';
import AppImage from './AppImage';
import AppLink from './AppLink';
import AppLoading from './AppLoading';
import ErrorBoundary from './ErrorBoundary';
export { ErrorBoundary, AppAlert, AppButton, AppIcon, AppIconButton, AppImage, AppLink, AppLoading };
export { ErrorBoundary, AppAlert, AppButton, AppIcon, AppIconButton, AppLink, AppLoading };
+10 -5
View File
@@ -2,19 +2,24 @@
* Components configuration
*/
export const CONTENT_MAX_WIDTH = 800;
export const CONTENT_MIN_WIDTH = 320; // CONTENT_MAX_WIDTH - Sidebar width
// The reading-width column (800) does not derive from the sidebar width (240,
// see layout/config.ts SIDE_BAR_WIDTH) — they're independent decisions. 320 is
// a standalone minimum so narrow viewports never squeeze content unreadably.
export const CONTENT_MIN_WIDTH = 320;
/**
* AppAlert and AppSnackBarAlert components
* AppAlert and AppSnackBarAlert components.
* A bare <AppAlert> defaults to a calm, non-alarming baseline — genuinely
* error-severity call-sites must pass severity="error" explicitly. `standard`
* pairs with the semantic palette (colors.ts) so inline alerts match toasts.
*/
export const APP_ALERT_SEVERITY = 'error'; // 'error' | 'info'| 'success' | 'warning'
export const APP_ALERT_VARIANT = 'filled'; // 'filled' | 'outlined' | 'standard'
export const APP_ALERT_SEVERITY = 'info'; // 'error' | 'info'| 'success' | 'warning'
export const APP_ALERT_VARIANT = 'standard'; // 'filled' | 'outlined' | 'standard'
/**
* AppButton component
*/
export const APP_BUTTON_VARIANT = 'contained'; // | 'text' | 'outlined'
export const APP_BUTTON_MARGIN = 1;
/**
* AppIcon component
@@ -70,7 +70,7 @@ const AddressCard: FunctionComponent<AddressCardProps> = ({
color="primary"
disabled={settingPrimary}
onClick={onSetPrimary}
sx={{ m: 0, mt: 0.25, alignSelf: 'flex-start' }}
sx={{ mt: 0.25, alignSelf: 'flex-start' }}
>
{setPrimaryLabel}
</AppButton>
@@ -149,11 +149,11 @@ const AddressForm: FunctionComponent<AddressFormProps> = ({ initial, submitting
<Stack direction="row" sx={{ gap: 1, justifyContent: 'flex-end' }}>
{onCancel ? (
<AppButton variant="text" onClick={onCancel} disabled={submitting} sx={{ m: 0 }}>
<AppButton variant="text" onClick={onCancel} disabled={submitting}>
{tc('cancel')}
</AppButton>
) : null}
<AppButton color="primary" variant="contained" onClick={handleSubmit} disabled={submitting} sx={{ m: 0 }}>
<AppButton color="primary" variant="contained" onClick={handleSubmit} disabled={submitting}>
{submitting ? tc('saving') : tc('save')}
</AppButton>
</Stack>
@@ -53,7 +53,6 @@ const BookingSupportEntry: FunctionComponent<BookingSupportEntryProps> = ({ book
color="primary"
startIcon="support"
onClick={() => setDialogOpen(true)}
sx={{ m: 0 }}
>
{t('open_from_booking')}
</AppButton>
@@ -48,7 +48,6 @@ const TicketInboxScreen: FunctionComponent<TicketInboxScreenProps> = ({ role })
color="primary"
startIcon="support"
onClick={() => setDialogOpen(true)}
sx={{ m: 0 }}
>
{t('contact_support')}
</AppButton>
@@ -43,7 +43,7 @@ const TicketThreadScreen: FunctionComponent<TicketThreadScreenProps> = ({ role,
return (
<Stack sx={{ gap: 2, maxWidth: 720, mx: 'auto', width: '100%' }}>
<AppButton variant="text" color="primary" onClick={goBack} sx={{ m: 0, alignSelf: 'flex-start' }}>
<AppButton variant="text" color="primary" onClick={goBack} sx={{ alignSelf: 'flex-start' }}>
{t('back_to_tickets')}
</AppButton>
@@ -61,7 +61,6 @@ const NotificationCenter: FunctionComponent<NotificationCenterProps> = ({ role }
color="primary"
onClick={() => markAll.mutate()}
disabled={markAll.isPending}
sx={{ m: 0 }}
>
{t('mark_all_read')}
</AppButton>